diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..455473148 Binary files /dev/null and b/.DS_Store differ diff --git a/Android.bp b/Android.bp index 13a926bc9..2c4fb3790 100644 --- a/Android.bp +++ b/Android.bp @@ -17,7 +17,19 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } -min_launcher3_sdk_version = "30" +min_launcher3_sdk_version = "31" + +// Targets that don't inherit framework aconfig libs (i.e., those that don't set +// `platform_apis: true`) must manually link them. +java_defaults { + name: "launcher-non-platform-apis-defaults", + static_libs: [ + "android.os.flags-aconfig-java", + "android.multiuser.flags-aconfig-java", + "android.appwidget.flags-aconfig-java", + "com.android.window.flags.window-aconfig-java", + ], +} // Common source files used to build launcher (java and kotlin) // All sources are split so they can be reused in many other libraries/apps in other folders @@ -31,12 +43,122 @@ filegroup { ], } +// Main Launcher source for compose, excluding the build config +filegroup { + name: "launcher-compose-enabled-src", + srcs: [ + "compose/facade/enabled/*.kt", + "compose/facade/core/*.kt", + "compose/features/**/*.kt", + ], +} + +filegroup { + name: "launcher-compose-disabled-src", + srcs: [ + "compose/facade/core/*.kt", + "compose/facade/disabled/*.kt", + ], +} + // Source code for quickstep build, on top of launcher-src filegroup { name: "launcher-quickstep-src", srcs: [ - "quickstep/src/**/*.java", "quickstep/src/**/*.kt", + "quickstep/src/**/*.java", + ], + device_common_srcs: [ + ":launcher-quickstep-processed-protolog-src", + ], +} + +// Launcher ProtoLog support +filegroup { + name: "launcher-quickstep-unprocessed-protolog-src", + srcs: [ + "quickstep/src_protolog/**/*.java", + ], +} + +java_library { + name: "launcher-quickstep_protolog-groups", + srcs: [ + "quickstep/src_protolog/**/*.java", + ], + static_libs: [ + "protolog-group", + "androidx.annotation_annotation", + "com_android_launcher3_flags_lib", + ], +} + +java_genrule { + name: "launcher-quickstep-processed-protolog-src", + srcs: [ + ":protolog-impl", + ":launcher-quickstep-unprocessed-protolog-src", + ":launcher-quickstep_protolog-groups", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) transform-protolog-calls " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " + + "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " + + "--viewer-config-file-path /system_ext/etc/launcher.quickstep.protolog.pb " + + "--output-srcjar $(out) " + + "$(locations :launcher-quickstep-unprocessed-protolog-src)", + out: ["launcher.quickstep.protolog.srcjar"], +} + +java_genrule { + name: "gen-launcher.quickstep.protolog.pb", + srcs: [ + ":launcher-quickstep-unprocessed-protolog-src", + ":launcher-quickstep_protolog-groups", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) generate-viewer-config " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " + + "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " + + "--viewer-config-type proto " + + "--viewer-config $(out) " + + "$(locations :launcher-quickstep-unprocessed-protolog-src)", + out: ["launcher.quickstep.protolog.pb"], +} + +prebuilt_etc { + name: "launcher.quickstep.protolog.pb", + system_ext_specific: true, + src: ":gen-launcher.quickstep.protolog.pb", + filename_from_src: true, +} + +// Source code for quickstep dagger +filegroup { + name: "launcher-quickstep-dagger", + srcs: [ + "quickstep/dagger/**/*.java", + "quickstep/dagger/**/*.kt", + ], +} + +// Source code for quickstep build with compose enabled, on top of launcher-src +filegroup { + name: "launcher-quickstep-compose-enabled-src", + srcs: [ + "quickstep/compose/facade/core/*.kt", + "quickstep/compose/facade/enabled/*.kt", + "quickstep/compose/features/**/*.kt", + ], +} + +filegroup { + name: "launcher-quickstep-compose-disabled-src", + srcs: [ + "quickstep/compose/facade/core/*.kt", + "quickstep/compose/facade/disabled/*.kt", ], } @@ -63,10 +185,118 @@ filegroup { srcs: ["proguard.flags"], } +// Opt-in configuration for Launcher3 code depending on Jetpack Compose. +soong_config_module_type { + name: "launcher_compose_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "srcs", + "static_libs", + ], +} + +// Opt-in configuration for Launcher Quickstep code depending on Jetpack Compose. +soong_config_bool_variable { + name: "release_enable_compose_in_launcher", +} + +soong_config_module_type { + name: "quickstep_compose_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "srcs", + "static_libs", + ], +} + +soong_config_module_type { + name: "launcher_compose_tests_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "static_libs", + ], +} + +launcher_compose_java_defaults { + name: "launcher_compose_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + srcs: [ + ":launcher-compose-enabled-src", + ], + + // Compose dependencies + static_libs: [ + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + ], + + // By default, Compose is disabled and we compile the ComposeFacade + // in compose/launcher3/facade/disabled/. + conditions_default: { + srcs: [ + ":launcher-compose-disabled-src", + ], + static_libs: [], + }, + }, + }, +} + +quickstep_compose_java_defaults { + name: "quickstep_compose_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + srcs: [ + ":launcher-quickstep-compose-enabled-src", + ], + + // Compose dependencies + static_libs: [ + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + ], + + // By default, Compose is disabled and we compile the ComposeFacade + // in compose/quickstep/facade/disabled/. + conditions_default: { + srcs: [ + ":launcher-quickstep-compose-disabled-src", + ], + static_libs: [], + }, + }, + }, +} + +launcher_compose_tests_java_defaults { + name: "launcher_compose_tests_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + // Compose dependencies + static_libs: [ + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + conditions_default: { + static_libs: [], + }, + }, + }, +} + android_library { name: "launcher-aosp-tapl", libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], static_libs: [ "androidx.annotation_annotation", @@ -76,6 +306,7 @@ android_library { "androidx.preference_preference", "SystemUISharedLib", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", "launcher-testing-shared", ], srcs: [ @@ -136,12 +367,14 @@ java_library { // Library with all the dependencies for building Launcher3 android_library { name: "Launcher3ResLib", + defaults: [ + "launcher_compose_defaults", + ], srcs: [], resource_dirs: ["res"], static_libs: [ "LauncherPluginLib", "launcher_quickstep_log_protos_lite", - "android.os.flags-aconfig-java", "androidx-constraintlayout_constraintlayout", "androidx.recyclerview_recyclerview", "androidx.dynamicanimation_dynamicanimation", @@ -154,7 +387,10 @@ android_library { "//frameworks/libs/systemui:iconloader_base", "//frameworks/libs/systemui:view_capture", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", + "//frameworks/libs/systemui:msdl", "SystemUI-statsd", + "WindowManager-Shell-shared-AOSP", "launcher-testing-shared", "androidx.lifecycle_lifecycle-common-java8", "androidx.lifecycle_lifecycle-extensions", @@ -163,8 +399,9 @@ android_library { "kotlinx_coroutines", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", - "android.appwidget.flags-aconfig-java", - "com.android.window.flags.window-aconfig-java", + "dagger2", + "jsr330", + "com_android_systemui_shared_flags_lib", ], manifest: "AndroidManifest-common.xml", sdk_version: "current", @@ -172,6 +409,9 @@ android_library { lint: { baseline_filename: "lint-baseline.xml", }, + flags_packages: [ + "com_android_launcher3_flags", + ], } // @@ -179,6 +419,7 @@ android_library { // android_app { name: "Launcher3", + defaults: ["launcher-non-platform-apis-defaults"], static_libs: [ "Launcher3ResLib", @@ -190,7 +431,7 @@ android_app { ], optimize: { - proguard_flags_files: ["proguard.flags"], + proguard_flags_files: [":launcher-proguard-rules"], // Proguard is disable for testing. Derivarive prjects to keep proguard enabled enabled: false, }, @@ -198,6 +439,7 @@ android_app { sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, target_sdk_version: "current", + plugins: ["dagger2-compiler"], privileged: true, system_ext_specific: true, @@ -214,8 +456,12 @@ android_app { "AndroidManifest-common.xml", ], lint: { + extra_check_modules: ["Launcher3LintChecker"], baseline_filename: "lint-baseline.xml", }, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Library with all the dependencies for building quickstep @@ -226,24 +472,34 @@ android_library { "quickstep/res", ], libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], static_libs: [ "Launcher3ResLib", "lottie", "SystemUISharedLib", "SettingsLibSettingsTheme", + "dagger2", + "protolog-group", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", + lint: { + disabled_checks: ["MissingClass"], + }, } // Library with all the source code and dependencies for building Launcher Go android_library { name: "Launcher3GoLib", + defaults: [ + "launcher_compose_defaults", + "quickstep_compose_defaults", + ], srcs: [ ":launcher-src", ":launcher-quickstep-src", + ":launcher-quickstep-dagger", "go/quickstep/src/**/*.java", "go/quickstep/src/**/*.kt", ], @@ -258,7 +514,10 @@ android_library { "QuickstepResLib", "androidx.room_room-runtime", ], - plugins: ["androidx.room_room-compiler-plugin"], + plugins: [ + "androidx.room_room-compiler-plugin", + "dagger2-compiler", + ], manifest: "quickstep/AndroidManifest.xml", additional_manifests: [ "go/AndroidManifest.xml", @@ -267,19 +526,27 @@ android_library { min_sdk_version: "current", // TODO(b/319712088): re-enable use_resource_processor use_resource_processor: false, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Library with all the source code and dependencies for building Quickstep android_library { name: "Launcher3QuickStepLib", + defaults: [ + "launcher_compose_defaults", + "quickstep_compose_defaults", + ], srcs: [ ":launcher-src", ":launcher-quickstep-src", + ":launcher-quickstep-dagger", ":launcher-build-config", ], resource_dirs: [], libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], // Note the ordering here is important when it comes to resource // overriding. We want the most specific resource overrides defined @@ -291,18 +558,23 @@ android_library { ], manifest: "quickstep/AndroidManifest.xml", platform_apis: true, + plugins: ["dagger2-compiler"], min_sdk_version: "current", // TODO(b/319712088): re-enable use_resource_processor use_resource_processor: false, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Build rule for Quickstep app. android_app { name: "Launcher3QuickStep", - static_libs: ["Launcher3QuickStepLib"], optimize: { - enabled: false, + proguard_flags_files: [":launcher-proguard-rules"], + enabled: true, + shrink_resources: true, }, platform_apis: true, @@ -316,7 +588,10 @@ android_app { "Launcher2", "Launcher3", ], - required: ["privapp_whitelist_com.android.launcher3"], + required: [ + "privapp_whitelist_com.android.launcher3", + "launcher.quickstep.protolog.pb", + ], resource_dirs: ["quickstep/res"], @@ -332,13 +607,11 @@ android_app { } - // Build rule for Launcher3 Go app with quickstep for Android Go devices. // Note that the following two rules are exactly same, and should // eventually be merged into a single target android_app { name: "Launcher3Go", - static_libs: ["Launcher3GoLib"], resource_dirs: [], @@ -349,6 +622,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, @@ -372,9 +646,9 @@ android_app { include_filter: ["com.android.launcher3.*"], }, } + android_app { name: "Launcher3QuickStepGo", - static_libs: ["Launcher3GoLib"], resource_dirs: [], @@ -385,6 +659,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 80d2eaca2..46f0e41cd 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -131,13 +131,11 @@ android:writePermission="${applicationId}.permission.WRITE_SETTINGS" android:readPermission="${applicationId}.permission.READ_SETTINGS" /> - + - - - \ No newline at end of file + + + + diff --git a/animationlib/src/com/android/app/animation/Animations.kt b/animationlib/src/com/android/app/animation/Animations.kt new file mode 100644 index 000000000..2b787fec8 --- /dev/null +++ b/animationlib/src/com/android/app/animation/Animations.kt @@ -0,0 +1,42 @@ +/* + * 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.app.animation + +import android.animation.Animator +import android.view.View + +/** A static class for general animation-related utilities. */ +class Animations { + companion object { + /** Stores a [view]'s ongoing [animation] so it can be cancelled if needed. */ + @JvmStatic + fun setOngoingAnimation(view: View, animation: Animator?) { + cancelOngoingAnimation(view) + view.setTag(R.id.ongoing_animation, animation) + } + + /** + * Cancels the ongoing animation affecting a [view], if any was previously stored using + * [setOngoingAnimation]. + */ + @JvmStatic + fun cancelOngoingAnimation(view: View) { + (view.getTag(R.id.ongoing_animation) as? Animator)?.cancel() + view.setTag(R.id.ongoing_animation, null) + } + } +} diff --git a/animationlib/tests/robolectric/config/robolectric.properties b/animationlib/tests/robolectric/config/robolectric.properties index 527eab687..fab7251d0 100644 --- a/animationlib/tests/robolectric/config/robolectric.properties +++ b/animationlib/tests/robolectric/config/robolectric.properties @@ -1,2 +1 @@ sdk=NEWEST_SDK -shadows=com.android.app.animation.robolectric.ShadowAnimationUtils2 diff --git a/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt b/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt deleted file mode 100644 index c3e74ee71..000000000 --- a/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.app.animation.robolectric - -import android.view.animation.AnimationUtils -import org.robolectric.annotation.Implements -import org.robolectric.shadows.ShadowAnimationUtils - -/** - * This shadow overwrites [ShadowAnimationUtils] and ensures that the real implementation of - * [AnimationUtils] is used in tests. - */ -@Implements(AnimationUtils::class) -class ShadowAnimationUtils2 diff --git a/animationlib/tests/src/com/android/app/animation/AnimationsTest.kt b/animationlib/tests/src/com/android/app/animation/AnimationsTest.kt new file mode 100644 index 000000000..decc503d7 --- /dev/null +++ b/animationlib/tests/src/com/android/app/animation/AnimationsTest.kt @@ -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 com.android.app.animation + +import android.animation.ValueAnimator +import android.content.Context +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AnimationsTest { + companion object { + const val TEST_DURATION = 1000L + } + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + @Test + fun ongoingAnimationsAreStoredAndCancelledCorrectly() { + val view = View(context) + + val oldAnimation = FakeAnimator() + Animations.setOngoingAnimation(view, oldAnimation) + oldAnimation.start() + assertEquals(oldAnimation, view.getTag(R.id.ongoing_animation)) + assertTrue(oldAnimation.started) + + val newAnimation = FakeAnimator() + Animations.setOngoingAnimation(view, newAnimation) + newAnimation.start() + assertEquals(newAnimation, view.getTag(R.id.ongoing_animation)) + assertTrue(oldAnimation.cancelled) + assertTrue(newAnimation.started) + + Animations.cancelOngoingAnimation(view) + assertNull(view.getTag(R.id.ongoing_animation)) + assertTrue(newAnimation.cancelled) + } +} + +/** Test animator for tracking start and cancel signals. */ +private class FakeAnimator : ValueAnimator() { + var started = false + var cancelled = false + + override fun start() { + started = true + cancelled = false + } + + override fun cancel() { + started = false + cancelled = true + } +} diff --git a/buglist.txt b/buglist.txt deleted file mode 100644 index 53dcc3559..000000000 --- a/buglist.txt +++ /dev/null @@ -1,42 +0,0 @@ -171450807 -170675311 -170338029 -170338170 -160544577 -171171594 -170488559 -171131394 -171131394 -171026321 -170648272 -170752716 -170611866 -170702596 -170487752 -170665892 -168608912 -170636685 -169771796 -141126144 -166614700 -168805872 -170263425 -169221288 -143965596 -169221287 -167259591 -156044202 -169438169 -164926736 -168653219 -169963211 -170121063 -169988381 -169980192 -169221288 -169385783 -168167693 -169796517 -169330678 -168818961 -168608912 diff --git a/buglist_unique.txt b/buglist_unique.txt deleted file mode 100644 index 93dbefb07..000000000 --- a/buglist_unique.txt +++ /dev/null @@ -1,39 +0,0 @@ -141126144 -143965596 -156044202 -160544577 -164926736 -166614700 -167259591 -168167693 -168608912 -168653219 -168805872 -168818961 -169221287 -169221288 -169330678 -169385783 -169438169 -169771796 -169796517 -169963211 -169980192 -169988381 -170121063 -170263425 -170338029 -170338170 -170487752 -170488559 -170611866 -170636685 -170648272 -170665892 -170675311 -170702596 -170752716 -171026321 -171131394 -171171594 -171450807 diff --git a/build.gradle b/build.gradle index a4c1c0380..829717930 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ android { defaultConfig { minSdkVersion 26 - targetSdkVersion 35 + targetSdkVersion 36 versionCode 1 versionName "1.0" @@ -49,8 +49,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } // The flavor dimensions for build variants (e.g. aospWithQuickstep, aospWithoutQuickstep) @@ -146,7 +146,7 @@ android { } } kotlinOptions { - jvmTarget = '17' + jvmTarget = '21' } buildFeatures { @@ -179,13 +179,14 @@ dependencies { // implementation project(':UiTestsLibLauncher') // withQuickstepImplementation project(':SharedLibWrapper') implementation 'com.google.protobuf:protobuf-javalite:4.26.1' - implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.core:core-ktx:1.16.0' // Recents lib dependency withQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}", include: 'sysui_shared.jar') // Required for AOSP to compile. This is already included in the sysui_shared.jar withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}", include: 'plugin_core.jar') + withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}", include: 'WindowManager-Shell-15.jar') testImplementation 'junit:junit:4.13.2' androidTestImplementation "org.mockito:mockito-core:1.9.5" @@ -196,10 +197,13 @@ dependencies { androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' androidTestImplementation "androidx.annotation:annotation:${ANDROID_X_VERSION}" - api 'com.airbnb.android:lottie:3.3.0' + api 'com.airbnb.android:lottie:6.6.6' + implementation 'com.google.dagger:dagger:2.56.2' + annotationProcessor 'com.google.dagger:dagger-compiler:2.56.2' - implementation 'com.google.android.material:material:1.10.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.slice:slice-builders:1.0.0' + implementation 'androidx.graphics:graphics-shapes:1.1.0-alpha01' implementation "androidx.window:window:1.3.0" } diff --git a/checks/.DS_Store b/checks/.DS_Store new file mode 100644 index 000000000..b39e95f88 Binary files /dev/null and b/checks/.DS_Store differ diff --git a/checks/Android.bp b/checks/Android.bp new file mode 100644 index 000000000..dfd701efe --- /dev/null +++ b/checks/Android.bp @@ -0,0 +1,46 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library_host { + name: "Launcher3LintChecker", + srcs: ["src/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + kotlincflags: ["-Xjvm-default=all"], +} + +java_test_host { + name: "Launcher3LintCheckerTest", + defaults: ["AndroidLintCheckerTestDefaults"], + srcs: ["tests/**/*.kt"], + data: [ + ":androidx.annotation_annotation", + ":dagger2", + ":kotlinx-coroutines-core", + ], + device_common_data: [ + ":framework", + ], + static_libs: [ + "Launcher3LintChecker", + ], +} diff --git a/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt new file mode 100644 index 000000000..37358bbc0 --- /dev/null +++ b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UClass + +/** Detector to identify custom usage of Android's Dialog within the Launcher3 codebase. */ +class CustomDialogDetector : Detector(), SourceCodeScanner { + + override fun applicableSuperClasses(): List { + return listOf(DIALOG_CLASS_NAME) + } + + override fun visitClass(context: JavaContext, declaration: UClass) { + val superTypeClassNames = declaration.superTypes.mapNotNull { it.resolve()?.qualifiedName } + if (superTypeClassNames.contains(DIALOG_CLASS_NAME)) { + context.report( + ISSUE, + declaration, + context.getNameLocation(declaration), + "Class implements Dialog", + ) + } + } + + companion object { + private const val DIALOG_CLASS_NAME = "android.app.Dialog" + + @JvmField + val ISSUE = + Issue.create( + id = "IllegalUseOfCustomDialog", + briefDescription = "dialogs should not be used in Launcher", + explanation = + """ + Don't use custom Dialogs within the launcher code base, instead consider utilizing + AbstractFloatingView to display content that should float above the launcher where + it can be correctly managed for dismissal. + """ + .trimIndent(), + category = Category.CORRECTNESS, + priority = 10, + severity = Severity.ERROR, + implementation = + Implementation(CustomDialogDetector::class.java, Scope.JAVA_FILE_SCOPE), + ) + } +} diff --git a/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt new file mode 100644 index 000000000..c77c42bf5 --- /dev/null +++ b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import CustomDialogDetector +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Issue +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class Launcher3IssueRegistry : IssueRegistry() { + override val issues: List + get() = listOf(CustomDialogDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + override val minApi: Int + get() = 8 + + override val vendor: Vendor = + Vendor( + vendorName = "Android", + feedbackUrl = "http://b/issues/new?component=78010", + contact = "abegovic@google.com", + ) +} diff --git a/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt new file mode 100644 index 000000000..2a3795303 --- /dev/null +++ b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import CustomDialogDetector +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +/** Test for [CustomDialogDetector]. */ +class CustomDialogDetectorTest : Launcher3LintDetectorTest() { + override fun getDetector(): Detector = CustomDialogDetector() + + override fun getIssues(): List = listOf(CustomDialogDetector.ISSUE) + + @Test + fun classDoesNotExtendDialog_noViolation() { + lint() + .files( + TestFiles.kotlin( + """ + package test.pkg + + class SomeClass + """ + .trimIndent() + ), + *androidStubs, + ) + .issues(CustomDialogDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun classDoesExtendDialog_violation() { + lint() + .files( + TestFiles.kotlin( + """ + package test.pkg + + import android.app.Dialog + + class SomeClass(context: Context) : Dialog(context) + """ + .trimIndent() + ), + *androidStubs, + ) + .issues(CustomDialogDetector.ISSUE) + .run() + .expect( + (""" + src/test/pkg/SomeClass.kt:5: Error: Class implements Dialog [IllegalUseOfCustomDialog] + class SomeClass(context: Context) : Dialog(context) + ~~~~~~~~~ + 1 errors, 0 warnings + """) + .trimIndent() + ) + } +} diff --git a/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt new file mode 100644 index 000000000..09085c7c9 --- /dev/null +++ b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask +import java.io.File +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Abstract class that should be used by any test for launcher 3 lint detectors. + * + * When you write your test, ensure that you pass [androidStubs] as part of your [TestFiles] + * definition. + */ +@RunWith(JUnit4::class) +abstract class Launcher3LintDetectorTest : LintDetectorTest() { + + /** + * Customize the lint task to disable SDK usage completely. This ensures that running the tests + * in Android Studio has the same result as running the tests in atest + */ + override fun lint(): TestLintTask = + super.lint().allowMissingSdk(true).sdkHome(File("/dev/null")) + + companion object { + private val libraryNames = + arrayOf( + "androidx.annotation_annotation.jar", + "dagger2.jar", + "framework.jar", + "kotlinx-coroutines-core.jar", + ) + + /** + * This file contains stubs of framework APIs and System UI classes for testing purposes + * only. The stubs are not used in the lint detectors themselves. + */ + val androidStubs = + libraryNames + .map { TestFiles.LibraryReferenceTestFile(File(it).canonicalFile) } + .toTypedArray() + } +} diff --git a/commitlist.txt b/commitlist.txt deleted file mode 100644 index 27b8baceb..000000000 --- a/commitlist.txt +++ /dev/null @@ -1,934 +0,0 @@ -COMMAND>> git log f99351888c3e5a128559678304fefd647472bc7f..4c3952dc60fc78d3816012a86d7e71747ef34c74 -commit 4c3952dc60fc78d3816012a86d7e71747ef34c74 -Merge: cb403d9e5 70e8b1572 -Author: TreeHugger Robot -Date: Fri Oct 23 00:27:39 2020 +0000 - - Merge "Minor stylistic changes in Workspace.java." into ub-launcher3-master - -commit cb403d9e5235c7323bc2fdffe6a264d17bb6d0a6 -Author: Pinyao Ting -Date: Thu Oct 22 16:07:08 2020 -0700 - - flip default value of minimal device feature flag - - Test: manual - Change-Id: Iaf46dffb935bdf4b46e7c57d547bdc697250ec56 - -commit a97557a15eb111616d868120a9f4659f1b451fa2 -Merge: f5ce80b8a 932a327eb -Author: Tracy Zhou -Date: Thu Oct 22 18:22:56 2020 +0000 - - Merge "Consider overscroll adjustment of RecentsView for live tile" into ub-launcher3-master - -commit 932a327ebf0587b8324b9fea7d31328b2f6719a8 -Author: Tracy Zhou -Date: Wed Oct 21 23:29:00 2020 -0700 - - Consider overscroll adjustment of RecentsView for live tile - - Fixes: 171450807 - Test: manual - Change-Id: I83eebf1f6b61c67f289db51aabe5a971815d0df1 - -commit f5ce80b8a0a1636fc8159475177a07b281492c88 -Author: Hilary Huo -Date: Wed Oct 14 16:35:55 2020 -0700 - - [pixel-search] Latency analysis, add logging statement in launcher - - Bug: b/170675311 - Change-Id: I229ace399085bea1c3f9535eb713edd329dff8bd - -commit 31b03941ef3aa17edc08c1b509d4fa23766f2d2c -Merge: e0a50c9e3 0731273d5 -Author: Tracy Zhou -Date: Wed Oct 21 20:03:57 2020 +0000 - - Merge "Track live tile better by considering resistance animation" into ub-launcher3-master - -commit 0731273d5409149fca32dfb2ad76eab45f6ea79a -Author: Tracy Zhou -Date: Wed Oct 21 12:03:40 2020 -0700 - - Track live tile better by considering resistance animation - - Fixes: 170338029 - Test: Manual - Change-Id: I66536bae567aa94385d5e0352cec9d46d512927a - -commit e0a50c9e3f1d4b9f113d6afae01ff2c4ed452fba -Merge: d2c27a595 acfac6187 -Author: Alex Chau -Date: Wed Oct 21 17:02:57 2020 +0000 - - Merge "Use Diplay.getMetrics in DisplayController" into ub-launcher3-master - -commit d2c27a595065d43bbea37dd2a512d37080f5233e -Merge: ff8febabb 8b488ccc2 -Author: Tracy Zhou -Date: Wed Oct 21 07:19:48 2020 +0000 - - Merge "[Live Tile] Support launching running task animation" into ub-launcher3-master - -commit 8b488ccc2e433a708c8b06f0b6866f2a305e4b0a -Author: Tracy Zhou -Date: Wed Oct 14 12:13:04 2020 -0700 - - [Live Tile] Support launching running task animation - - Fixes: 170338170 - Test: manual - Change-Id: I2526b7cfbacaea7899b8e2ed233f913630071d36 - -commit 70e8b157219e9090ba5e47fdfa51b2b92e98449d -Author: Andy Wickham -Date: Wed Oct 7 23:00:06 2020 -0700 - - Minor stylistic changes in Workspace.java. - - Change-Id: Ib07611f27cbc427d11abccd8b74ea144485752f7 - -commit acfac6187dd9d13d55b566a77a5da867a1813573 -Author: Alex Chau -Date: Mon Oct 19 18:00:39 2020 +0100 - - Use Diplay.getMetrics in DisplayController - - - This is a workaround of b/163815566, where DisplayMetrics is stale - when onDisplayChanged is called. - - Instead of relying on stale DisplayConext, get the DisplayMetrics - from the Display directly. - - Also optimized how DisplayController.Info is created by passing in - Display only - - Use mDisplayContext.getDisplay directly if availalbe - - Bug: 163815566, 160544577 - Test: DPI looks correct on device boot - Change-Id: I2a7454bb8cf2073ce592e8662781b87fc998444f - (cherry picked from commit 177c38243dc3bf245d1f7db3c265dfb56522f441) - -commit ff8febabb039a3c27ee068f85119860a048b917c -Merge: b03d2b416 102746823 -Author: TreeHugger Robot -Date: Tue Oct 20 17:46:09 2020 +0000 - - Merge "Makes Plugin Settings gear adjust to dark mode." into ub-launcher3-master - -commit b03d2b41616d479ba360fa4f97e57722c7f57b8e -Merge: fb79f5541 caa1e9c39 -Author: Hyunyoung Song -Date: Tue Oct 20 15:25:56 2020 +0000 - - Merge "Search query method should support multiple consumers" into ub-launcher3-master - -commit fb79f5541dcbe587002756bb40a3c632d38cc25a -Merge: f6b05068d cf0b275a4 -Author: Schneider Victor-tulias -Date: Tue Oct 20 13:51:06 2020 +0000 - - Merge "Add the ability to specify a list of tutorial steps in the gesture sandbox tutorial intent." into ub-launcher3-master - -commit f6b05068d901d4e989b2e107c06f9c7a6e7b113f -Author: Hyunyoung Song -Date: Tue Oct 20 00:19:29 2020 -0700 - - Invert the badging - - Bug: 171171594 - Change-Id: If84fdc03254105c843e16f39f479505b16e1cd5f - -commit caa1e9c39978cb3b467b5ac441eb39b5e883fa2e -Author: Hyunyoung Song -Date: Mon Oct 12 13:56:02 2020 -0700 - - Search query method should support multiple consumers - - Bug: 170488559 - Change-Id: I64bef9523d3c3950c4ca3a4b9ce1d506d1672200 - -commit 10274682339bb60cb24c50536b4f48f921970f3c -Author: Andy Wickham -Date: Mon Oct 19 19:06:52 2020 -0700 - - Makes Plugin Settings gear adjust to dark mode. - - It wasn't visible in dark mode before because it was - black on black. This makes it adjust automatically. - - Change-Id: I5176cffc01842509ddafc4f30ff5029a0c4b8050 - -commit 744a0fbeae8efaa942d21c61e25012d86f5ff81e -Merge: 29c79947e 71f24588c -Author: TreeHugger Robot -Date: Mon Oct 19 22:17:26 2020 +0000 - - Merge "Call click event on IME quick select for SearchResultIcon" into ub-launcher3-master - -commit 29c79947ecf82f662d02004ba9a7289017fc0783 -Merge: 13a2a010d a68ac3e5d -Author: TreeHugger Robot -Date: Mon Oct 19 18:42:05 2020 +0000 - - Merge "Removing condition for CUJ tracing/metrics" into ub-launcher3-master - -commit 71f24588c0a66449a0c68bcb360a8c671914ce75 -Author: Samuel Fufa -Date: Mon Oct 19 10:19:31 2020 -0700 - - Call click event on IME quick select for SearchResultIcon - - Bug: 171131394 - Change-Id: I8a703e8d0ca10570e3f774510610d3fb4c0eaab8 - -commit 13a2a010decd87eeaf8932430c692f587d2de165 -Author: Samuel Fufa -Date: Sun Oct 18 21:19:57 2020 -0700 - - Handle IME event for SearchResultIcon - - Bug: 171131394 - Test: Manual - Change-Id: I2ed1c61053c78aaecc3324418229d69634a72ae4 - -commit 1f79eeda76246534697e92740defc7f73c3c8d14 -Author: Samuel Fufa -Date: Fri Oct 16 02:01:31 2020 -0700 - - Remove hardcoded itemTypes from SearchTarget - - - Introduces componentName and userHandle members to SearchTarget - - SearchTargetEvent now has searchTarget member - - Builder pattern for SearchTarget and SearchTargetEvent - - Search backend should add headers manually instead of launcher inferring sections - - Bug: 171026321 - Test: Manual - Change-Id: I28e0455e82b925277a17703b9aa061c8f9f15262 - -commit a68ac3e5dd23095cea7c872c0ff1c5042d1695ba -Author: vadimt -Date: Fri Oct 16 10:48:28 2020 -0700 - - Removing condition for CUJ tracing/metrics - - Is doesn't reflect whether jank monitors is collecting metrics, - which will eventually be always true anyways. - - Change-Id: Iaebdc838ed2b2cebd32c8c48d7e45bdd93f76fb4 - -commit 9228ff53c2fb26850b7bd92d86214a6aaebb11d3 -Author: Sunny Goyal -Date: Mon Oct 12 13:43:51 2020 -0700 - - Trimming activity and task label - - Bug: 170648272 - Change-Id: Icd099acee65305e0aa0f98a2a301a0df8a27cf07 - -commit 7a09177e500a53205f9969bb6cbd4251d54e8fde -Merge: 37ed5ead3 314761a80 -Author: TreeHugger Robot -Date: Thu Oct 15 22:36:14 2020 +0000 - - Merge "Setup SearchResultIcon for single cell results" into ub-launcher3-master - -commit 37ed5ead391df5747003b2d3a345be0347362f19 -Merge: d5bbe6809 702ed2788 -Author: TreeHugger Robot -Date: Thu Oct 15 22:06:12 2020 +0000 - - Merge "Fix the issue where shortcuts are removed in minimal device mode" into ub-launcher3-master - -commit 314761a80819a6e64a136161f51eebb0f0528c4d -Author: Samuel Fufa -Date: Wed Oct 14 10:15:07 2020 -0700 - - Setup SearchResultIcon for single cell results - - SearchResultIcon will be able to render apps, shortcuts and remote actions. It can also handle its own focused state drawing. - - Screenshot: https://screenshot.googleplex.com/C3KgjJtLQTBPgaf - - Bug: 170752716 - Test: Manual - Change-Id: I460a9c128ea3f5814784e342c5d5fa5b7e310882 - -commit 702ed2788678ac744c768aad6a6302e7cf91a26b -Author: Pinyao Ting -Date: Wed Oct 14 11:17:04 2020 -0700 - - Fix the issue where shortcuts are removed in minimal device mode - - When loading the workspace, Launcher pins/unpins shortcuts in comply - with the loaded workspace. Since minimal device mode creates a mostly - empty workspace, existing shortcuts are getting unpinned as a result. - - To mitigate the issue this CL compares the db name and only invoke - sanitizeData when it matches the one defined in InvariantDeviceProfile. - - Bug: 170611866 - Test: manual - 1. add some deep shortcut in workspace (e.g. long tap on chrome, drag - "incognito tab" to workspace) - 2. opt-in to sunshine fishfood (g/sunshine-teamfood) - 3. enable bedtime mode with minimal device in Settings -> Digital - Wellbeing -> Show Your Data -> Bedtime mode -> Customize -> minimal - device - 4. toggle bedtime mode, wait for apps in minimal device to show, then - toggle off bedtime mode - 5. verify the deep shortcut still exist - - Change-Id: Ie18216ecb288e7481aa2404c4cb3ea418aee85cb - -commit cf0b275a48d3c9f91a346f7fc24b9604f6dde25a -Author: Schneider Victor-tulias -Date: Tue Oct 6 09:33:40 2020 -0400 - - Add the ability to specify a list of tutorial steps in the gesture sandbox tutorial intent. - - Added tutorial_steps string array in the intent to allow specifying an ordered list of tutorial steps. - - Change-Id: Ic42a65598a74a64f8441a22f58c6cd988a5762e3 - -commit d5bbe6809dcc056fbfc307909b171651f0fb3044 -Author: Samuel Fufa -Date: Wed Oct 14 15:39:38 2020 -0700 - - Rename shrotcut container to deep-shrotcuts - - Change-Id: If94f0dfa447235f3b1a652f7b6c749695b42d97c - -commit 26c1105fa04c2bcc156051e51df90a6a253349bb -Author: Samuel Fufa -Date: Tue Oct 13 01:12:03 2020 -0700 - - [search api part 1] Setup centralized SearchEventTracker - - - Rename AdapterItemWIthPayload to SearchAdapterItem, PayloadResultHandler to SearchTargetHandler - - Setup SliceViewWrapper for self contained slices - - Bug: 170702596 - Change-Id: I0baf984ec8123c95011abcc17372f8d055e98ad7 - -commit 057f2d0d7df67e3680e479ac76b48b30d8bcf884 -Merge: 4bb65ff51 9a6145efb -Author: TreeHugger Robot -Date: Tue Oct 13 01:57:47 2020 +0000 - - Merge "Introduce shortcut container for hotseat event reporting" into ub-launcher3-master - -commit 4bb65ff516c6d9a429971ab7e04780792d5cb751 -Merge: 69740e62b 2afcab804 -Author: TreeHugger Robot -Date: Tue Oct 13 00:07:36 2020 +0000 - - Merge "Search UI clean up" into ub-launcher3-master - -commit 2afcab804b638ff3b9da5bad40c8f70bdcaae78d -Author: Samuel Fufa -Date: Mon Oct 12 15:38:14 2020 -0700 - - Search UI clean up - - - Resolve spacing issue when work profile is installed - - Cache play icons and use icon shape - - Only draw focus indicator for the first result - - Bug: 170487752 - Bug: 170665892 - Change-Id: I864d2e796786637132e127ef9b418c0a76c74d6e - -commit 69740e62be3800fc918648009645f7a8e52cb73d -Merge: 2d7bfc878 979da64d8 -Author: TreeHugger Robot -Date: Mon Oct 12 20:53:29 2020 +0000 - - Merge "Add app start source info of apps launched from launcher" into ub-launcher3-master - -commit 2d7bfc8782e9ed01178672aeb09ba2a6a07f4f4c -Author: Jon Miranda -Date: Mon Oct 12 12:09:22 2020 -0700 - - Fix shadowRadius not being used in swipe up animation. - - Bug: 168608912 - Change-Id: I08f7bb057237e5061d5f1fc29afb488b204ee385 - -commit a433fe1fb34715efb38ed094f39da49fce8cd51e -Merge: 2de606fe7 0471b9836 -Author: Sunny Goyal -Date: Mon Oct 12 18:08:35 2020 +0000 - - Merge "Using FrameCallbacks instead of windowCallbacks for surface removal" into ub-launcher3-master - -commit 9a6145efb85f2bbdaccc07166a55e22c15fe27db -Author: Samuel Fufa -Date: Mon Oct 12 09:33:00 2020 -0700 - - Introduce shortcut container for hotseat event reporting - - Bug: 170636685 - Test: Manual - Change-Id: I5abeb17976bbafdc8cc74fb8b9a586d544c682fc - -commit 2de606fe731573c081fd2d6ba166e21ea6aa2e9c -Author: Yogisha Dixit -Date: Mon Oct 12 15:36:07 2020 +0100 - - Delete the minimal database to force refresh. - - Bug: 169771796 - Test: manual - Change-Id: Ic2188bb162f295c208346861fddc137ace19ddcb - -commit 0471b9836c9e382dc14bdc3abdf8502fb2b9f266 -Author: Sunny Goyal -Date: Wed Sep 23 13:54:37 2020 -0700 - - Using FrameCallbacks instead of windowCallbacks for surface removal - - WindowCallbacks is called during the draw pass, before the frame has - been sent to the surfaceFlinger. Frame callback will provide a closer - approximation for when the frame is actually rendered on screen. - - Bug: 141126144 - Change-Id: I62aab526c2ca24b00b5e7b312b36080f26c7b439 - -commit 2727434c44d06882925369bf4b43687a06be4a3f -Merge: 59f532fe9 1b9e199b3 -Author: Schneider Victor-tulias -Date: Fri Oct 9 20:09:08 2020 +0000 - - Merge "Fix hotseat and prediction row to allow updates when empty." into ub-launcher3-master - -commit 59f532fe9e2b1817c094641f3c7c517f42e4faf0 -Merge: d2bfce71f b5334e3f0 -Author: TreeHugger Robot -Date: Fri Oct 9 19:52:54 2020 +0000 - - Merge "Improve search section header" into ub-launcher3-master - -commit 979da64d8254599c332d83bf94f3f1fc3fe45fef -Author: Riddle Hsu -Date: Tue Sep 22 21:52:40 2020 +0800 - - Add app start source info of apps launched from launcher - - Bug: 166614700 - Test: Enable statsd log: "adb shell cmd stats print-logs" - adb logcat | grep statsd | grep "(48)" - The line may contain 0x100000->1[I] 0x110000->10[I] - that means 1=from launcher and 10=latency 10ms. - Change-Id: Iddaff7066b66e241ba58ec87129ddbe2c531dc7e - (cherry picked from commit 7bdf3574a3bff06a377b4364877687bfa7619d06) - -commit d2bfce71f776fd05633dfd915dfc664309274677 -Merge: ed4530fed 222afb970 -Author: Winson Chung -Date: Fri Oct 9 16:39:06 2020 +0000 - - Merge "Comply with the ISystemUiProxy.aidl change" into ub-launcher3-master - -commit ed4530fedda0bf876f91d0745fc70d0f30d42991 -Merge: 692d2109a 9d4a96ed0 -Author: Winson Chung -Date: Fri Oct 9 16:39:06 2020 +0000 - - Merge "Add latency metrics for recents gesture" into ub-launcher3-master - -commit 1b9e199b3d9c81c793758d96bb03e0c51c1b3fb1 -Author: Schneider Victor-tulias -Date: Thu Oct 8 15:50:22 2020 -0400 - - Fix hotseat and prediction row to allow updates when empty. - - Rotating the screen in the homescreen empties the hotseat, however it does not get populated while it is visible to the user. The user should not be able to see an empty hotseat or prediction row if predictions are available. It should therefore be possible to populate these when they are empty even if they are visible to the user. - - Change-Id: I8e5252bd29050c2cd9d443aedcb3f3e305c0e2d7 - -commit b5334e3f07f0561808a2d6e9bba55f1e3a89191e -Author: Hyunyoung Song -Date: Fri Oct 9 00:50:48 2020 -0700 - - Improve search section header - - Change-Id: I47cf207f0d0ab792c0e7a47c9d1185eec087ec88 - -commit 692d2109a6702706d24b3b819d115882f7362509 -Author: Samuel Fufa -Date: Thu Oct 8 18:42:48 2020 -0700 - - invalidate itemDecoration on predictedRow focus draw - - Change-Id: I66c731f00ae1c1292c51ff281957f05fd2d70dfa - -commit 8d5b118060bff7f7518a9a14c0be5d265621f14c -Author: Samuel Fufa -Date: Thu Oct 8 13:11:25 2020 -0700 - - Revert PredictionRow shuoldDraw check - - + Show Rounded play result icons - - Bug: 168805872 - Test: Manual - Change-Id: I663c7f7ca1f1ac072e5e9c441deabef7c3fbd97b - -commit 86f8df6cf954ac27ab092b9ef8a4db3c9979c4cb -Merge: 4d19854b2 16045060c -Author: Hilary Huo -Date: Thu Oct 8 18:43:51 2020 +0000 - - Merge "[pixel-search] add escape hatch" into ub-launcher3-master - -commit 4d19854b25a54599fe9b0ac8be9d60cf6c21d7ba -Merge: 0827e1e32 ab9ad20be -Author: Samuel Fufa -Date: Thu Oct 8 18:40:56 2020 +0000 - - Merge "Search UI cleanup" into ub-launcher3-master - -commit 16045060c35639aea85afc572bea768d16e6c9f9 -Author: Hilary Huo -Date: Thu Oct 8 10:18:41 2020 -0700 - - [pixel-search] add escape hatch - - Change-Id: I33ffea1fc0859564955380d7d1db317293d1a2cb - -commit 0827e1e32a5f99fa02418dae37270c6db8c989d2 -Merge: 3463f0a87 68d7a6e5b -Author: Andy Wickham -Date: Thu Oct 8 16:53:29 2020 +0000 - - Merge "Adds feature flag for BC Smartspace." into ub-launcher3-master - -commit ab9ad20be600d1cbdc6b54a491d5fbb4c2cf9c16 -Author: Samuel Fufa -Date: Wed Oct 7 15:18:24 2020 -0700 - - Search UI cleanup - - - offset all apps header padding with search input margin - - avoid check shouldDraw check on HeaderRow. (race condition) - - Bug: 170263425 - Change-Id: I11a1fbb448aa6afd18ec0984af9bb8b1d7600f69 - -commit 68d7a6e5b28af8cc55bdae7efc24cc7ebee81257 -Author: Andy Wickham -Date: Wed Oct 7 14:27:17 2020 -0700 - - Adds feature flag for BC Smartspace. - - Change-Id: Iaf9fb7507d0ccd004a4e00188c75dadd6a059246 - -commit 3463f0a876ff486ce03e160134e0504158271a92 -Merge: 2470d812a 4b7f38b8f -Author: TreeHugger Robot -Date: Wed Oct 7 20:09:04 2020 +0000 - - Merge "Align fallback result query with result text" into ub-launcher3-master - -commit 2470d812a1ae989e67781e5056b534ad9a960819 -Merge: cae7d74d8 7a6e4c931 -Author: Vadim Tryshev -Date: Wed Oct 7 20:04:09 2020 +0000 - - Merge "Annotating Quick Switch CUJ for 3-button mode" into ub-launcher3-master - -commit cae7d74d898769727105850ea5473c2c0ae25fdb -Merge: e9bf2bd14 1fddddb4f -Author: Tony Wickham -Date: Wed Oct 7 18:32:48 2020 +0000 - - Merge "Update launcher_trace.proto for quick switch" into ub-launcher3-master - -commit 7a6e4c931f13b369bfa4328196b4632d6d848a19 -Author: vadimt -Date: Tue Oct 6 14:09:16 2020 -0700 - - Annotating Quick Switch CUJ for 3-button mode - - Bug: 169221288 - Change-Id: Ief62345fe6004dde699f44aa0c90329b7cd84e8b - -commit 4b7f38b8fa004b514244304fcc07ff514a2fa46b -Author: Samuel Fufa -Date: Tue Oct 6 18:37:46 2020 -0700 - - Align fallback result query with result text - - screenshot: https://screenshot.googleplex.com/6Daj5vdmz2jmznX - bug: 169438169 - test: Manual - Change-Id: Ie621ed3c834aec5e9467607da4f685d05d152183 - -commit 222afb970434c7972589adfc509bd2c256ca6556 -Author: Hongwei Wang -Date: Fri Oct 2 13:51:36 2020 -0700 - - Comply with the ISystemUiProxy.aidl change - - Two methods are added to support communications between Launcher and - SysUI when user swipes an auto PiP-able Activity to home. - - Bug: 143965596 - Test: N/A - Change-Id: I2c73a287a094e882bde3cd71c27f9f66ae20e64a - (cherry picked from commit 88ddae38db924f700082a113670ce5a719116a95) - -commit 9d4a96ed029fdad1e369d5eedd082938f0dc9e01 -Author: Riddle Hsu -Date: Wed Sep 30 00:32:04 2020 +0800 - - Add latency metrics for recents gesture - - Pass the touch down time to RecentsAnimation#startRecentsActivity. - - Bug: 169221287 - Test: Enable statsd log: "adb shell cmd stats print-logs" - Touch gesture navigation bar. - adb logcat | grep statsd | grep "(48)" - The line may contain 0x100000->4[I] 0x110000->20[I] - that means 4=by recents and 20=latency 20ms. - Change-Id: I81ee804895b7712f4d925736f5b4694c11a12cbe - (cherry picked from commit 63623967b83edad56db58173ebb6687c685b9177) - -commit e9bf2bd14c9a7a48f8f93687932d41b1418cf4e4 -Merge: 73ae75474 d028937e7 -Author: Tracy Zhou -Date: Wed Oct 7 02:19:04 2020 +0000 - - Merge "[Live tile] Finish recents animation when the phone goes to sleep in live tile mode" into ub-launcher3-master - -commit 1fddddb4f30505e0fc9bb2e7c0d88b38ad900e54 -Author: Tony Wickham -Date: Tue Sep 29 17:29:06 2020 -0700 - - Update launcher_trace.proto for quick switch - - Sample output from one entry: - entry { - elapsed_realtime_nanos: 440461382888540 - launcher { - touch_interaction_service { - service_connected: true - overview_component_obvserver { - overview_activity_started: true - overview_activity_resumed: false - } - input_consumer { - name: "TYPE_OTHER_ACTIVITY:TYPE_ONE_HANDED" - swipe_handler { - gesture_state { - endTarget: NEW_TASK - } - is_recents_attached_to_app_window: true - scroll_offset: 846 - app_to_overview_progress: 0 - } - } - } - } - } - - Bug: 167259591 - Change-Id: I7f199d88f1d736efcea6b9165b8c4b77a5d27c58 - -commit 73ae75474ec1dd8807d814ea6c22323905d2070c -Merge: 8a6f3e40d 0ebbc1880 -Author: TreeHugger Robot -Date: Tue Oct 6 23:18:44 2020 +0000 - - Merge "Removing tracing for a gone flake" into ub-launcher3-master - -commit 8a6f3e40d0321217c624055db7929c397e455e0c -Merge: e29a9f796 565ed4ff6 -Author: TreeHugger Robot -Date: Tue Oct 6 22:49:40 2020 +0000 - - Merge "Update Search UI" into ub-launcher3-master - -commit 0ebbc18803aaf8ef2f6db7d628d7ae1ce322e842 -Author: vadimt -Date: Tue Oct 6 14:52:27 2020 -0700 - - Removing tracing for a gone flake - - Bug: 156044202 - Change-Id: Ice142bb941fee7b731f46c2073fab17d83bbc871 - -commit 565ed4ff69b534812818a2b9aa8789a1aea210eb -Author: Samuel Fufa -Date: Wed Sep 30 10:42:07 2020 -0700 - - Update Search UI - - [preview attached to bug] - - Bug: 169438169 - Test: Manual - Change-Id: I085f3dd38ac373c1afab82a637ec08715a6e0cc5 - -commit e29a9f7961e6db0915bc028ef7e871dcb2c8bde0 -Merge: 2c5ed10ff be17bdcd2 -Author: Jayaprakash Sundararaj -Date: Tue Oct 6 21:00:20 2020 +0000 - - Merge "[Search] Add logging to People and badding as to icons." into ub-launcher3-master - -commit be17bdcd221f501c45876abe2249c1007858d0c0 -Author: jayaprakashs -Date: Mon Oct 5 09:01:52 2020 -0700 - - [Search] Add logging to People and badding as to icons. - - Change-Id: I65948a2faca436216a94aa46139d425b8eade827 - -commit 2c5ed10ffa1a870de35f9b3c0c558270aff498dd -Merge: b2b65a1ef 8ed9707cf -Author: Tracy Zhou -Date: Tue Oct 6 18:40:57 2020 +0000 - - Merge "[Live Tile] Support launching another task (other than the current running task) in Overview" into ub-launcher3-master - -commit b2b65a1ef58b020923d112051535b6eb83b582df -Merge: 3cf264f49 4c14f4b9e -Author: Samuel Fufa -Date: Tue Oct 6 17:45:35 2020 +0000 - - Merge "Avoid double search item highlight" into ub-launcher3-master - -commit 8ed9707cf3a4300cb61942f08f0752c80eed086b -Author: Tracy Zhou -Date: Mon Sep 14 23:25:37 2020 -0700 - - [Live Tile] Support launching another task (other than the current running task) in Overview - - - Get rid of the defer cancelation logic - - Render animation on the task view of the task being launched upon task view appeared callback - - Finish the recents animation upon the end of the recents window animation - - Fixes: 164926736 - Test: manual - Change-Id: Ibffb6a9c74c235efc8615a22b0306551532c7b61 - -commit 3cf264f498e37c482fa4c559bf48ffa791279585 -Author: Schneider Victor-tulias -Date: Tue Sep 22 12:58:38 2020 -0700 - - Prevent hotseat updates if it is visible to the user. - - Test: manual - - Fixes: 168653219 - - Changing app icons under the user's finger could be disruptive. Added a checks for whether the hotseatand all apps predictions are visible and callbacks to update them when they become hidden. - - Change-Id: Ib9e6e904e9f662ecfaeea6a2fe21d1d81ba39b96 - -commit b6aff1f56d55a36256446ec3970d92e9da39b98c -Author: Hyunyoung Song -Date: Mon Oct 5 16:08:35 2020 -0700 - - Fix NPE inside RecentsOrientedState - - Bug: 169963211 - Change-Id: I86dd337dc1b862f3fa99b91b47fa250076233f96 - -commit eab40983b9a48b933bde5ca95a82ebd4d83b233d -Merge: 83ce7c0b5 020e628f2 -Author: Jonathan Miranda -Date: Mon Oct 5 22:20:27 2020 +0000 - - Merge "Add shadow radius to windows during app launch / close animations." into ub-launcher3-master - -commit 83ce7c0b5e461386bb92883a8d6cefe8365cd9ae -Merge: 679d920bf d6b1f3c08 -Author: TreeHugger Robot -Date: Mon Oct 5 19:18:39 2020 +0000 - - Merge "Action icon should be used as a badge instead of main icon" into ub-launcher3-master - -commit 679d920bf5151cffed4e8186c12c25d8d7907af9 -Merge: e108cc609 0c943966d -Author: TreeHugger Robot -Date: Mon Oct 5 19:13:50 2020 +0000 - - Merge "Add null check for input receiver before updating batching" into ub-launcher3-master - -commit e108cc609d0a7fd58f0c7e16ce45fa79be6dd272 -Merge: 470403eb5 f622e42bf -Author: TreeHugger Robot -Date: Mon Oct 5 18:39:58 2020 +0000 - - Merge "Removing unused proto extensions" into ub-launcher3-master - -commit 470403eb58879380e2edac2262dc7f40327b2a15 -Merge: a5130482a 1d7ed30db -Author: TreeHugger Robot -Date: Mon Oct 5 18:29:54 2020 +0000 - - Merge "Remove widgets that no longer fit the workspace in their current spans." into ub-launcher3-master - -commit 4c14f4b9eda8332347c81e0cf51c5de4dbc06399 -Author: Samuel Fufa -Date: Mon Oct 5 10:50:00 2020 -0700 - - Avoid double search item highlight - - Change-Id: Ic2e28b18f6d5e3ed32cd5646bc3bb4789c378e57 - -commit 0c943966d373d8ae7eef2b08e88ac44bf57d8a8d -Author: Winson Chung -Date: Mon Oct 5 10:23:27 2020 -0700 - - Add null check for input receiver before updating batching - - - A change in the system (ie. sysui crash or nav mode change) could - cause the input monitor to be disposed before the swipe animation - settles - - Bug: 170121063 - Test: Kill sysui while swiping up - - Change-Id: I1417b109fecdb98fae6197c7038dbe9307470853 - -commit a5130482aee1b0592661bc1c6e178a0de7a163da -Merge: b21819e18 7fcd74abb -Author: TreeHugger Robot -Date: Mon Oct 5 17:14:21 2020 +0000 - - Merge "Suggest result should launch Bug: 169980192" into ub-launcher3-master - -commit d028937e74a9ea6d36e463de4c87ed37283bbdf6 -Author: Tracy Zhou -Date: Sat Oct 3 00:36:53 2020 -0700 - - [Live tile] Finish recents animation when the phone goes to sleep in live tile mode - - Fixes: 169988381 - Test: manual - Change-Id: Ic71d3e6767eadb6854dbd46581bf9d3242c161a4 - -commit 7fcd74abb399100ac8243be6ca28c09cc8adc8c8 -Author: Hyunyoung Song -Date: Fri Oct 2 19:20:11 2020 -0700 - - Suggest result should launch - Bug: 169980192 - - Change-Id: I762245a5cc4740d093c9cb3b44a508e9e3f2b763 - -commit b21819e181e99504c22c6ca028261a1f2665c6f9 -Merge: 931bce369 a762b0241 -Author: TreeHugger Robot -Date: Fri Oct 2 22:07:12 2020 +0000 - - Merge "Annotating Quick Switch CUJ for non-3-button modes" into ub-launcher3-master - -commit a762b02418695f5a1ff2f96586660de8c3610280 -Author: vadimt -Date: Fri Oct 2 13:56:28 2020 -0700 - - Annotating Quick Switch CUJ for non-3-button modes - - Bug: 169221288 - Change-Id: I7145a9e28a2f0a789d19d2a0e3d15630c6e50f6a - -commit 931bce3697595a214023bc72923dad47a61d5711 -Merge: c935ba6b8 733e3c609 -Author: TreeHugger Robot -Date: Fri Oct 2 19:19:50 2020 +0000 - - Merge "Moving some initializations to the background thread" into ub-launcher3-master - -commit c935ba6b8a2ec163533c0b19309dacb6199e6552 -Merge: a4111f250 58804ac52 -Author: Sunny Goyal -Date: Fri Oct 2 18:26:06 2020 +0000 - - Merge "Adding stats log for add item flow" into ub-launcher3-master - -commit 733e3c609b7653a36e58747c881458ec00d98df8 -Author: Sunny Goyal -Date: Tue Sep 29 10:32:32 2020 -0700 - - Moving some initializations to the background thread - - HandlerThread.getLooper blocks until the thread is ready. Instead - moving all looper dependency to the new thread itself. - - Change-Id: I240e8c56b855a991433a7fe93875059e6dab146b - -commit 58804ac5257f45dddbf7a6db35cf8f369ee1e88e -Author: Sunny Goyal -Date: Wed Sep 16 16:27:40 2020 -0700 - - Adding stats log for add item flow - - Bug: 169385783 - Bug: 168167693 - Change-Id: I37395f1b118727f67e0f14c02f945b8213b165c8 - -commit a4111f250003328d1aef8bbaab59512208ec46cb -Merge: 8d14dbe04 f6b72c4ad -Author: Hilary Huo -Date: Fri Oct 2 17:41:22 2020 +0000 - - Merge "[pixel-search] Bug fix: automatically launch screenshot + center&crop remoteaction icon" into ub-launcher3-master - -commit f622e42bf6983d3adb95386bfd6375d281f1d4f2 -Author: Sunny Goyal -Date: Fri Oct 2 10:35:56 2020 -0700 - - Removing unused proto extensions - - Change-Id: I6d0319c99934dad5176b6f70b895a4ca772ec45f - -commit d6b1f3c086f9ac097cd03e1ee898b153478ec11a -Author: Hyunyoung Song -Date: Fri Oct 2 00:26:35 2020 -0700 - - Action icon should be used as a badge instead of main icon - - Bug: 169796517 - Change-Id: I3f07fdc2ae6e1af463701f942c26c3ca5d836ee2 - -commit f6b72c4ad1d2e082441a64c4d6a5a02ee8a251ca -Author: Hilary Huo -Date: Thu Oct 1 12:26:48 2020 -0700 - - [pixel-search] Bug fix: automatically launch screenshot + center&crop remoteaction icon - - Bug: b/169330678 - Change-Id: Id5f8a0ce6d68f7ed9e4d1ff258ee3772229eb63b - -commit 1d7ed30dba4b2c71fc7b0981532a872a13e5aedb -Author: Jon Miranda -Date: Wed Sep 23 12:15:43 2020 -0700 - - Remove widgets that no longer fit the workspace in their current spans. - - This can happen when display size changes. - We compare span sizes of widget in the db to the min sizes of the widget - in the current display size. If the widget can no longer fit in its existing - spans, we remove it. - - Also update test widgets to have minWidth/minHeight of 1dp. This ensures that - the spanX, spanY, min* values remain consistent between different test devices. - - Bug: 168818961 - Change-Id: I723372e4582658f78b2f23ced9073cb77977a6b8 - -commit 020e628f22cc7975beab439c6da26af2f9ebc15b -Author: Jon Miranda -Date: Mon Sep 28 17:01:42 2020 -0700 - - Add shadow radius to windows during app launch / close animations. - - Bug: 168608912 - Change-Id: I2ec50b0b3711c0861659f9c641bbc05fcdeaab45 diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/compose/facade/core/BaseComposeFacade.kt similarity index 71% rename from quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt rename to compose/facade/core/BaseComposeFacade.kt index a8b511286..bc7ba4700 100644 --- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt +++ b/compose/facade/core/BaseComposeFacade.kt @@ -14,11 +14,13 @@ * limitations under the License. */ -package com.android.quickstep.task.viewmodel +package com.android.launcher3.compose.core -import kotlinx.coroutines.flow.MutableStateFlow +import android.content.Context +import android.view.View -class TaskViewData { - // This is typically a View concern but it is used to invalidate rendering in other Views - val scale = MutableStateFlow(1f) +interface BaseComposeFacade { + fun isComposeAvailable(): Boolean + + fun initComposeView(appContext: Context): View } diff --git a/compose/facade/disabled/ComposeFacade.kt b/compose/facade/disabled/ComposeFacade.kt new file mode 100644 index 000000000..c1cbfff0c --- /dev/null +++ b/compose/facade/disabled/ComposeFacade.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.launcher3.compose + +import android.content.Context +import android.view.View +import com.android.launcher3.compose.core.BaseComposeFacade + +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = false + + override fun initComposeView(appContext: Context): View { + error( + "Compose is not available. Make sure to check isComposeAvailable() before calling any" + + " other function on ComposeFacade." + ) + } +} diff --git a/compose/facade/enabled/ComposeFacade.kt b/compose/facade/enabled/ComposeFacade.kt new file mode 100644 index 000000000..d98a979f2 --- /dev/null +++ b/compose/facade/enabled/ComposeFacade.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.launcher3.compose + +import android.content.Context +import android.view.View +import androidx.compose.ui.platform.ComposeView +import com.android.launcher3.compose.core.BaseComposeFacade + +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = true + + override fun initComposeView(appContext: Context): View = ComposeView(appContext) +} diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml index 2223036a7..bef7180cb 100644 --- a/go/AndroidManifest-launcher.xml +++ b/go/AndroidManifest-launcher.xml @@ -57,6 +57,7 @@ android:enabled="true"> + diff --git a/go/go.iml b/go/go.iml deleted file mode 100644 index c78484703..000000000 --- a/go/go.iml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/go/quickstep/res/values-af/strings.xml b/go/quickstep/res/values-af/strings.xml index 501d297a8..f4ce47658 100644 --- a/go/quickstep/res/values-af/strings.xml +++ b/go/quickstep/res/values-af/strings.xml @@ -1,7 +1,7 @@ - "Deel program" + "Deel app" "Luister" "Vertaal" "Lens" @@ -9,12 +9,12 @@ "KANSELLEER" "INSTELLINGS" "Vertaal of luister na teks op skerm" - "Inligting soos teks op jou skerm, webadresse en skermskote kan met Google gedeel word.\n\nGaan na ""Instellings > Programme > Verstekprogramme > Digitale Assistent-program"" om te verander watter inligting jy deel." + "Inligting soos teks op jou skerm, webadresse en skermskote kan met Google gedeel word.\n\nGaan na ""Instellings > Apps > Verstekapps > Digitale Assistent-app"" om te verander watter inligting jy deel." "Kies \'n assistent om hierdie kenmerk te gebruik" "Kies \'n digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" "Verander jou assistent om hierdie kenmerk te gebruik" "Verander jou digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" "Tik hier om na teks op hierdie skerm te luister" "Tik hier om teks op hierdie skerm te vertaal" - "Hierdie program kan nie gedeel word nie" + "Hierdie app kan nie gedeel word nie" diff --git a/go/quickstep/res/values-be/strings.xml b/go/quickstep/res/values-be/strings.xml index 83374bb39..de8766f70 100644 --- a/go/quickstep/res/values-be/strings.xml +++ b/go/quickstep/res/values-be/strings.xml @@ -4,7 +4,7 @@ "Абагуліць праграму" "Праслухаць" "Перакласці" - "Аб\'ектыў" + "Аб’ектыў" "ЗРАЗУМЕЛА" "СКАСАВАЦЬ" "НАЛАДЫ" diff --git a/go/quickstep/res/values-fa/strings.xml b/go/quickstep/res/values-fa/strings.xml index 8453d4e3e..f0e4a5771 100644 --- a/go/quickstep/res/values-fa/strings.xml +++ b/go/quickstep/res/values-fa/strings.xml @@ -5,7 +5,7 @@ "گوش دادن" "ترجمه" "لنز" - "متوجه‌ام" + "متوجهم" "لغو" "تنظیمات" "ترجمه نوشتار روی صفحه‌نمایش یا گوش دادن به آن" diff --git a/go/quickstep/res/values-iw/strings.xml b/go/quickstep/res/values-iw/strings.xml index ddb8ddd9a..db661066e 100644 --- a/go/quickstep/res/values-iw/strings.xml +++ b/go/quickstep/res/values-iw/strings.xml @@ -14,7 +14,7 @@ "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לבחור אפליקציית עוזר דיגיטלי ב\'הגדרות\'" "צריך לשנות את העוזר הדיגיטלי כדי להשתמש בתכונה הזו" "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לשנות את אפליקציית העוזר הדיגיטלי ב\'הגדרות\'" - "צריך להקיש כאן כדי להאזין לטקסט שבמסך הזה" - "צריך להקיש כאן כדי לתרגם את הטקסט שבמסך הזה" + "צריך ללחוץ כאן כדי להאזין לטקסט שבמסך הזה" + "צריך ללחוץ כאן כדי לתרגם את הטקסט שבמסך הזה" "אי אפשר לשתף את האפליקציה הזו" diff --git a/go/quickstep/res/values-ne/strings.xml b/go/quickstep/res/values-ne/strings.xml index e66f06373..4f771c3ec 100644 --- a/go/quickstep/res/values-ne/strings.xml +++ b/go/quickstep/res/values-ne/strings.xml @@ -9,11 +9,11 @@ "रद्द गर्नुहोस्" "सेटिङ" "स्क्रिनमा देखिने पाठ अनुवाद गर्नुहोस् वा पढेर सुनाउनुहोस्" - "तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न ""सेटिङ > एप > डिफल्ट एप > डिजिटल सहायक एप"" मा जानुहोस्।" + "तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न ""सेटिङ > एप > डिफल्ट एप > डिजिटल एसिस्टेन्ट एप"" मा जानुहोस्।" "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने कुनै सहायक छनौट गर्नुहोस्" - "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप छनौट गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप छनौट गर्नुहोस्" "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने आफ्नो सहायक परिवर्तन गर्नुहोस्" - "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप परिर्वर्तन गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप परिर्वर्तन गर्नुहोस्" "तपाईं यो स्क्रिनमा देखिने पाठ सुन्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" "तपाईं यो स्क्रिनमा देखिने पाठ अनुवाद गर्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" "यो एप अरूलाई चलाउन दिन मिल्दैन" diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml index c659331bd..2524c760f 100644 --- a/go/quickstep/res/values/styles.xml +++ b/go/quickstep/res/values/styles.xml @@ -16,7 +16,7 @@ --> - + \ No newline at end of file diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml index ca44a6914..ea6f39c1e 100644 --- a/quickstep/res/values-nl/strings.xml +++ b/quickstep/res/values-nl/strings.xml @@ -22,11 +22,13 @@ "Vastzetten" "Vrije vorm" "Desktop" + "Verplaatsen naar extern scherm" + "Sluiten" + "Desktop" "Geen recente items" "Instellingen voor app-gebruik" "Alles wissen" "Recente apps" - "Taak gesloten" "%1$s, %2$s" "< 1 minuut" "Nog %1$s vandaag" @@ -45,6 +47,7 @@ "App-suggesties staan aan" "App-suggesties staan uit" "Voorspelde app: %1$s" + "Tutorial voor navigatie met gebaren" "Het apparaat draaien" "Draai het apparaat om de tutorial voor navigatie met gebaren af te ronden" "Swipe vanaf de rechter- of linkerrand" @@ -56,17 +59,15 @@ "Open Instellingen om de gevoeligheid van Terug te wijzigen" "Swipe om terug te gaan" "Swipe vanaf de linker- of rechterrand naar het midden om terug te gaan naar het vorige scherm." - "Als je wilt teruggaan naar het laatste scherm, swipe je met 2 vingers vanaf de linker- of rechterrand naar het midden van het scherm." "Terug" "Swipe vanaf de linker- of rechterrand naar het midden van het scherm" "Swipe vanaf de onderrand van het scherm omhoog" "Pauzeer niet voordat je loslaat" "Swipe recht omhoog" "Je weet nu hoe je het gebaar Naar startscherm maakt. Ontdek als volgende hoe je kunt teruggaan." - "Je weet nu hoe je teruggaat naar het startscherm" + "Je weet nu hoe je het gebaar Naar het startscherm maakt" "Swipe om naar het startscherm te gaan" "Swipe omhoog vanaf de onderkant van het scherm. Met dit gebaar ga je altijd naar het startscherm." - "Swipe met 2 vingers omhoog vanaf de onderkant van het scherm. Met dit gebaar ga je altijd naar het startscherm." "Naar het startscherm" "Swipe omhoog vanaf de onderkant van het scherm" "Goed gedaan" @@ -77,7 +78,6 @@ "Je weet nu hoe je het gebaar Schakelen tussen apps maakt" "Swipe om tussen apps te schakelen" "Swipe omhoog vanaf de onderkant van het scherm, houd vast en laat los om tussen apps te schakelen." - "Swipe met 2 vingers omhoog vanaf de onderkant van het scherm, houd vast en laat los om tussen apps te schakelen." "Schakelen tussen apps" "Swipe omhoog vanaf de onderkant van het scherm, houd je vinger op het scherm en laat dan los" "Goed bezig" @@ -99,7 +99,7 @@ "App-paar opslaan" "Tik op nog een app om je scherm te splitsen" "Kies een andere app om gesplitst scherm te gebruiken" - "Annuleren" + "Annuleren" "Sluit de selectie voor gesplitst scherm" "Kies andere app om gesplitst scherm te gebruiken" "Deze actie wordt niet toegestaan door de app of je organisatie" @@ -130,18 +130,37 @@ "Snelle instellingen" "Taakbalk" "Taakbalk wordt getoond" - "Taakbalk is verborgen" + "Taakbalk en bubbels links getoond" + "Taakbalk en bubbels rechts getoond" "Navigatiebalk" "Taakbalk altijd tonen" "Navigatiemodus wijzigen" "Scheiding voor Taakbalk" + "Taakbalkoverloop" "Naar boven/links verplaatsen" "Naar beneden/rechts verplaatsen" - "{count,plural, =1{Nog # app tonen.}other{Nog # apps tonen.}}" - "{count,plural, =1{# desktop-app tonen.}other{# desktop-apps tonen.}}" + "App openen als ballon" + "Recente apps" + "Lijst met recente apps" + "{count,plural, =1{extra app}other{extra apps}}" + "Desktop" "%1$s en %2$s" + "%1$s, item %2$d van %3$d" + "Naar links scrollen" + "Naar rechts scrollen" "Bubbel" "Overloop" "%1$s van %2$s" "%1$s en nog %2$d" + "Naar links verplaatsen" + "Naar rechts verplaatsen" + "Alles sluiten" + "%1$s uitvouwen" + "%1$s samenvouwen" + "Circle to Search" + "Icoon van app" + "Titel van app" + "Knop Sluiten" + "Vastzetten op taakbalk" + "Losmaken van taakbalk" diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml index bf0bdc832..e3664bc00 100644 --- a/quickstep/res/values-or/strings.xml +++ b/quickstep/res/values-or/strings.xml @@ -22,11 +22,13 @@ "ପିନ୍‍" "ଫ୍ରିଫର୍ମ" "ଡେସ୍କଟପ" + "ଏକ୍ସଟର୍ନଲ ଡିସପ୍ଲେକୁ ମୁଭ କରନ୍ତୁ" + "ବନ୍ଦ କରନ୍ତୁ" + "ଡେସ୍କଟପ" "ବର୍ତ୍ତମାନର କୌଣସି ଆଇଟମ ନାହିଁ" "ଆପ ବ୍ୟବହାର ସେଟିଂସ" "ସବୁ ଖାଲି କରନ୍ତୁ" "ବର୍ତ୍ତମାନର ଆପ୍‌" - "ଟାସ୍କ ବନ୍ଦ ହୋଇଯାଇଛି" "%1$s %2$s" "< 1 ମିନିଟ୍" "ଆଜି %1$s ବାକି ଅଛି" @@ -45,18 +47,18 @@ "ଆପ୍ ପରାମର୍ଶଗୁଡ଼ିକୁ ସକ୍ଷମ କରାଯାଇଛି" "ଆପ୍ ପରାମର୍ଶଗୁଡ଼ିକୁ ଅକ୍ଷମ କରାଯାଇଛି" "ପୂର୍ବାନୁମାନ କରାଯାଇଥିବା ଆପ୍: %1$s" + "ଜେଶ୍ଚର ନାଭିଗେସନ ଟ୍ୟୁଟୋରିଆଲ" "ଆପଣଙ୍କ ଡିଭାଇସକୁ ରୋଟେଟ କରନ୍ତୁ" "ଜେଶ୍ଚର ନାଭିଗେସନ ଟ୍ୟୁଟୋରିଆଲ ସମ୍ପୂର୍ଣ୍ଣ କରିବାକୁ ଦୟାକରି ଆପଣଙ୍କ ଡିଭାଇସ ରୋଟେଟ କରନ୍ତୁ" - "ଆପଣ ସ୍କ୍ରିନର ଏକଦମ୍-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ୍ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" + "ଆପଣ ସ୍କ୍ରିନର ଏକଦମ-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" "ଆପଣ ସ୍କ୍ରିନର ଡାହାଣ ବା ବାମ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରି ଛାଡ଼ି ଦେଉଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ଆପଣ ଡାହାଣରୁ ସ୍ୱାଇପ୍ କରି ପଛକୁ କିପରି ଫେରିବେ ତାହା ଜାଣିଲେ। ତା\'ପରେ, ଆପକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି। ତା\'ପରେ, ଆପଗୁଡ଼ିକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" - "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି" + "ଆପଣ ପଛକୁ ଫେରନ୍ତୁ ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି" "ଆପଣ ସ୍କ୍ରିନର ତଳଭାଗର ଅତି ନିକଟରୁ ସ୍ୱାଇପ କରୁନଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ପଛକୁ ଫେରିବା ଜେଶ୍ଚରର ସମ୍ବେଦନଶୀଳତା ବଦଳାଇବାକୁ ସେଟିଂସକୁ ଯାଆନ୍ତୁ" "ପଛକୁ ଫେରିବା ପାଇଁ ସ୍ୱାଇପ୍ କରନ୍ତୁ" "ପୂର୍ବ ସ୍କ୍ରିନକୁ ଫେରିବା ପାଇଁ, ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରନ୍ତୁ।" - "ପୂର୍ବ ସ୍କ୍ରିନକୁ ଫେରିବା ପାଇଁ, ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ 2ଟି ଆଙ୍ଗୁଠିରେ ସ୍ୱାଇପ କରନ୍ତୁ।" "ପଛକୁ ଫେରନ୍ତୁ" "ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରନ୍ତୁ" "ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" @@ -66,20 +68,18 @@ "ଆପଣ \'ମୂଳପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" "ହୋମକୁ ଯିବା ପାଇଁ ସ୍ୱାଇପ କରନ୍ତୁ" "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ। ଏହି ଜେଶ୍ଚର ସର୍ବଦା ଆପଣଙ୍କୁ ହୋମ ସ୍କ୍ରିନକୁ ନେଇଥାଏ।" - "2ଟି ଆଙ୍ଗୁଠିରେ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ। ଏହି ଜେଶ୍ଚର ସର୍ବଦା ଆପଣଙ୍କୁ ହୋମ ସ୍କ୍ରିନକୁ ନେଇଥାଏ।" "ହୋମକୁ ଯାଆନ୍ତୁ" "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ" "ବଢ଼ିଆ କାମ!" "ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ୱିଣ୍ଡୋକୁ ରିଲିଜ୍ କରିବା ପୂର୍ବରୁ ଅଧିକ ସମୟ ଧରି ରଖିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ।" - "ଆପଣ ସିଧା ଉପରକୁ ସ୍ୱାଇପ୍ କରି ତା\'ପରେ ବିରତ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" + "ଆପଣ ସିଧା ଉପରକୁ ସ୍ୱାଇପ କରି ତା\'ପରେ ବିରତ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" "ଜେଶ୍ଚରଗୁଡ଼ିକୁ କିପରି ବ୍ୟବହାର କରାଯିବ ଆପଣ ତାହା ଶିଖିଛନ୍ତି। ଜେଶ୍ଚରଗୁଡ଼ିକୁ ବନ୍ଦ କରିବାକୁ, ସେଟିଂସକୁ ଯାଆନ୍ତୁ।" - "ଆପଣ \'ଆପଗୁଡ଼ିକୁ ସ୍ୱିଚ୍ କରନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" + "ଆପଣ ଆପ୍ସ ସୁଇଚ କରନ୍ତୁ ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" "ଆପଗୁଡ଼ିକୁ ସ୍ୱିଚ୍ କରିବା ପାଇଁ ସ୍ୱାଇପ୍ କରନ୍ତୁ" "ଆପ୍ସ ମଧ୍ୟରେ ସୁଇଚ କରିବାକୁ, ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ, ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କରନ୍ତୁ।" - "ଆପ୍ସ ମଧ୍ୟରେ ସ୍ୱିଚ କରିବାକୁ, 2ଟି ଆଙ୍ଗୁଠିରେ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କର।" "ଆପ୍ସ ସୁଇଚ କରନ୍ତୁ" - "ଆପଣଙ୍କ ସ୍କ୍ରିନ୍‌ର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ୍ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ୍ କରନ୍ତୁ" + "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କରନ୍ତୁ" "ବହୁତ ବଢ଼ିଆ!" "ସବୁ ପ୍ରସ୍ତୁତ" "ହୋଇଗଲା" @@ -99,7 +99,7 @@ "ଆପ ପେୟାର ସେଭ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" - "ବାତିଲ କରନ୍ତୁ" + "ବାତିଲ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" "ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ" @@ -130,18 +130,37 @@ "କୁଇକ ସେଟିଂସ" "ଟାସ୍କବାର" "ଟାସ୍କବାର ଦେଖାଯାଇଛି" - "ଟାସ୍କବାର ଲୁଚାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ବାମରେ ଦେଖାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ଡାହାଣରେ ଦେଖାଯାଇଛି" "ନାଭିଗେସନ ବାର" "ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ" "ନାଭିଗେସନ ମୋଡ ପରିବର୍ତ୍ତନ କରନ୍ତୁ" "ଟାସ୍କବାର ଡିଭାଇଡର" + "ଟାସ୍କବାର ଓଭରଫ୍ଲୋ" "ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ" "ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ" - "{count,plural, =1{ଅଧିକ #ଟି ଆପ ଦେଖାନ୍ତୁ।}other{ଅଧିକ #ଟି ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" - "{count,plural, =1{# ଡେସ୍କଟପ ଆପ ଦେଖାନ୍ତୁ।}other{# ଡେସ୍କଟପ ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" + "ଏକ ବବଲ ଭାବେ ଆପ ଖୋଲନ୍ତୁ" + "ବର୍ତ୍ତମାନର ଆପ" + "ବର୍ତ୍ତମାନର ଆପ ତାଲିକା" + "{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}" + "ଡେସ୍କଟପ" "%1$s ଏବଂ %2$s" + "%1$s, %3$dର %2$d ଆଇଟମ" + "ବାମକୁ ସ୍କ୍ରୋଲ କରନ୍ତୁ" + "ଡାହାଣକୁ ସ୍କ୍ରୋଲ କରନ୍ତୁ" "ବବଲ" "ଓଭରଫ୍ଲୋ" "%2$sରୁ %1$s" "%1$s ଏବଂ ଅଧିକ %2$d" + "ବାମକୁ ମୁଭ କରନ୍ତୁ" + "ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ" + "ସବୁ ଖାରଜ କରନ୍ତୁ" + "%1$s ବିସ୍ତାର କରନ୍ତୁ" + "%1$s ସଙ୍କୁଚିତ କରନ୍ତୁ" + "ସର୍ଚ୍ଚ କରିବାକୁ ସର୍କଲ କରନ୍ତୁ" + "ଆପ ଆଇକନ" + "ଆପ ଟାଇଟେଲ" + "\"ବନ୍ଦ କରନ୍ତୁ\" ବଟନ" + "ଟାସ୍କବାରରେ ପିନ କର" + "ଟାସ୍କବାରରୁ ଅନପିନ କର" diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml index fc603969a..bcf1683d7 100644 --- a/quickstep/res/values-pa/strings.xml +++ b/quickstep/res/values-pa/strings.xml @@ -22,11 +22,13 @@ "ਪਿੰਨ ਕਰੋ" "ਫ੍ਰੀਫਾਰਮ" "ਡੈਸਕਟਾਪ" + "ਬਾਹਰੀ ਡਿਸਪਲੇ \'ਤੇ ਜਾਓ" + "ਬੰਦ ਕਰੋ" + "ਡੈਸਕਟਾਪ" "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮ ਨਹੀਂ" "ਐਪ ਵਰਤੋਂ ਦੀਆਂ ਸੈਟਿੰਗਾਂ" "ਸਭ ਕਲੀਅਰ ਕਰੋ" "ਹਾਲੀਆ ਐਪਾਂ" - "ਕਾਰਜ ਬੰਦ" "%1$s, %2$s" "< 1 ਮਿੰਟ" "ਅੱਜ %1$s ਬਾਕੀ" @@ -45,6 +47,7 @@ "ਐਪ ਸੁਝਾਵਾਂ ਨੂੰ ਚਾਲੂ ਕੀਤਾ ਗਿਆ" "ਐਪ ਸੁਝਾਵਾਂ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ" "ਪੂਰਵ ਅਨੁਮਾਨਿਤ ਐਪ: %1$s" + "ਇਸ਼ਾਰਾ ਨੈਵੀਗੇਸ਼ਨ ਸੰਬੰਧੀ ਟਿਊਟੋਰੀਅਲ" "ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਘੁੰਮਾਓ" "ਕਿਰਪਾ ਕਰਕੇ ਇਸ਼ਾਰਾ ਨੈਵੀਗੇਸ਼ਨ ਟਿਊਟੋਰੀਅਲ ਨੂੰ ਪੂਰਾ ਕਰਨ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਘੁੰਮਾਓ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸੱਜੇ ਜਾਂ ਖੱਬੇ ਪਾਸੇ ਦੇ ਬਿਲਕੁਲ ਕਿਨਾਰੇ ਤੋਂ ਸਵਾਈਪ ਕਰਦੇ ਹੋ" @@ -56,7 +59,6 @@ "ਪਿੱਛੇ ਜਾਣ ਦੇ ਸੰਕੇਤ ਦੀ ਸੰਵੇਦਨਸ਼ੀਲਤਾ ਬਦਲਣ ਲਈ, ਸੈਟਿੰਗਾਂ \'ਤੇ ਜਾਓ" "ਪਿੱਛੇ ਜਾਣ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਪਿਛਲੀ ਸਕ੍ਰੀਨ \'ਤੇ ਵਾਪਸ ਜਾਣ ਲਈ, ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ ਸਵਾਈਪ ਕਰੋ।" - "ਪਿਛਲੀ ਸਕ੍ਰੀਨ \'ਤੇ ਵਾਪਸ ਜਾਣ ਲਈ, ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ 2 ਉਂਗਲਾਂ ਨਾਲ ਸਵਾਈਪ ਕਰੋ।" "ਵਾਪਸ ਜਾਓ" "ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ ਸਵਾਈਪ ਕਰੋ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਲੇ ਕਿਨਾਰੇ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" @@ -66,7 +68,6 @@ "ਤੁਸੀਂ \'ਹੋਮ \'ਤੇ ਜਾਓ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" "ਹੋਮ \'ਤੇ ਜਾਣ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ। ਇਹ ਇਸ਼ਾਰਾ ਹਮੇਸ਼ਾਂ ਤੁਹਾਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਲੈ ਜਾਂਦਾ ਹੈ।" - "ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ 2 ਉਂਗਲਾਂ ਨਾਲ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ। ਇਹ ਇਸ਼ਾਰਾ ਹਮੇਸ਼ਾਂ ਤੁਹਾਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਲੈ ਜਾਂਦਾ ਹੈ।" "ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਜਾਓ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" "ਬਹੁਤ ਵਧੀਆ!" @@ -74,10 +75,9 @@ "ਛੱਡਣ ਤੋਂ ਪਹਿਲਾਂ ਵਿੰਡੋ ਨੂੰ ਕੁਝ ਸਮੇਂ ਲਈ ਦਬਾ ਕੇ ਰੱਖੋ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸਿੱਧਾ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ, ਫਿਰ ਰੋਕੋ" "ਤੁਸੀਂ ਇਸ਼ਾਰੇ ਵਰਤਣ ਬਾਰੇ ਜਾਣਿਆ। ਇਸ਼ਾਰੇ ਬੰਦ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ \'ਤੇ ਜਾਓ।" - "ਤੁਸੀਂ \'ਐਪਾਂ ਵਿਚਾਲੇ ਅਦਲਾ-ਬਦਲੀ ਕਰੋ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" + "ਤੁਸੀਂ \'ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰੋ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" "ਐਪਾਂ ਵਿਚਾਲੇ ਅਦਲਾ-ਬਦਲੀ ਕਰਨ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰਨ ਲਈ, ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਤੇ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ ਅਤੇ ਫਿਰ ਛੱਡੋ।" - "ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰਨ ਲਈ, ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ 2 ਉਂਗਲਾਂ ਨਾਲ ਉੱਤੇ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ ਅਤੇ ਫਿਰ ਛੱਡੋ।" "ਐਪਾਂ ਸਵਿੱਚ ਕਰੋ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ, ਅਤੇ ਫਿਰ ਛੱਡੋ" "ਬਹੁਤ ਵਧੀਆ!" @@ -90,7 +90,7 @@ "ਪੂਰੀ ਤਰ੍ਹਾਂ ਤਿਆਰ!" "ਹੋਮ \'ਤੇ ਜਾਣ ਲਈ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" "ਆਪਣੀ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਜਾਣ ਲਈ ਹੋਮ ਬਟਨ \'ਤੇ ਟੈਪ ਕਰੋ" - "ਤੁਸੀਂ ਆਪਣਾ %1$s ਵਰਤਣ ਲਈ ਤਿਆਰ ਹੋ" + "ਹੁਣ ਤੁਹਾਡਾ %1$s ਵਰਤੋਂ ਲਈ ਤਿਆਰ ਹੈ" "ਡੀਵਾਈਸ" "ਸਿਸਟਮ ਨੈਵੀਗੇਸ਼ਨ ਸੈਟਿੰਗਾਂ" "ਸਾਂਝਾ ਕਰੋ" @@ -99,7 +99,7 @@ "ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" - "ਰੱਦ ਕਰੋ" + "ਰੱਦ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" "ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ" @@ -130,18 +130,37 @@ "ਤਤਕਾਲ ਸੈਟਿੰਗਾਂ" "ਟਾਸਕਬਾਰ" "ਟਾਸਕਬਾਰ ਨੂੰ ਦਿਖਾਇਆ ਗਿਆ" - "ਟਾਸਕਬਾਰ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਖੱਬੇ ਦਿਖਾਇਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਸੱਜੇ ਦਿਖਾਇਆ" "ਨੈਵੀਗੇਸ਼ਨ ਵਾਲੀ ਪੱਟੀ" "ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਓ" "ਨੈਵੀਗੇਸ਼ਨ ਮੋਡ ਬਦਲੋ" "ਟਾਸਕਬਾਰ ਵਿਭਾਜਕ" + "ਟਾਸਕਬਾਰ ਓਵਰਫ਼ਲੋ" "ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" "ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" - "{count,plural, =1{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}one{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}other{# ਹੋਰ ਐਪਾਂ ਦਿਖਾਓ।}}" - "{count,plural, =1{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}one{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}other{# ਡੈਸਕਟਾਪ ਐਪਾਂ ਦਿਖਾਓ।}}" + "ਐਪ ਨੂੰ ਬਬਲ ਵਜੋਂ ਖੋਲ੍ਹੋ" + "ਹਾਲੀਆ ਐਪਾਂ" + "ਹਾਲੀਆ ਐਪ ਸੂਚੀ" + "{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}" + "ਡੈਸਕਟਾਪ" "%1$s ਅਤੇ %2$s" + "%1$s, %3$d ਵਿੱਚੋਂ %2$d ਆਈਟਮ" + "ਖੱਬੇ ਪਾਸੇ ਵੱਲ ਸਕ੍ਰੋਲ ਕਰੋ" + "ਸੱਜੇ ਪਾਸੇ ਵੱਲ ਸਕ੍ਰੋਲ ਕਰੋ" "ਬਬਲ" "ਓਵਰਫ਼ਲੋ" "%2$s ਤੋਂ %1$s" "%1$s ਅਤੇ %2$d ਹੋਰ" + "ਖੱਬੇ ਲਿਜਾਓ" + "ਸੱਜੇ ਲਿਜਾਓ" + "ਸਭ ਖਾਰਜ ਕਰੋ" + "%1$s ਦਾ ਵਿਸਤਾਰ ਕਰੋ" + "%1$s ਨੂੰ ਸਮੇਟੋ" + "ਖੋਜਣ ਲਈ ਚੱਕਰ ਬਣਾਓ" + "ਐਪ ਪ੍ਰਤੀਕ" + "ਐਪ ਸਿਰਲੇਖ" + "\'ਬੰਦ ਕਰੋ\' ਬਟਨ" + "ਟਾਸਕਬਾਰ \'ਤੇ ਪਿੰਨ ਕਰੋ" + "ਟਾਸਕਬਾਰ ਤੋਂ ਅਣਪਿੰਨ ਕਰੋ" diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml index 13c8c1c48..14b60c49d 100644 --- a/quickstep/res/values-pl/strings.xml +++ b/quickstep/res/values-pl/strings.xml @@ -22,11 +22,13 @@ "Przypnij" "Tryb dowolny" "Pulpit" + "Przenieś na wyświetlacz zewnętrzny" + "Zamknij" + "Pulpit" "Brak ostatnich elementów" "Ustawienia użycia aplikacji" "Wyczyść wszystko" "Ostatnie aplikacje" - "Zadanie zamknięte" "%1$s, %2$s" "> 1 min" "Na dziś zostało %1$s" @@ -45,6 +47,7 @@ "Włączono sugestie aplikacji" "Sugestie aplikacji są wyłączone" "Przewidywana aplikacja: %1$s" + "Samouczek dotyczący nawigacji przy użyciu gestów" "Obróć urządzenie" "Obróć urządzenie, aby ukończyć samouczek nawigacji przy użyciu gestów" "Pamiętaj, aby przesuwać palcem od samej krawędzi (prawej lub lewej)" @@ -56,7 +59,6 @@ "Czułość gestu cofania możesz zmienić w Ustawieniach" "Przesuń palcem, aby przejść wstecz" "Aby wrócić do poprzedniego ekranu, przesuń palcem od lewej lub prawej krawędzi do środka ekranu." - "Aby wrócić do poprzedniego ekranu, przesuń 2 palcami od lewej lub prawej krawędzi do środka ekranu." "Przejście wstecz" "Przesuń palcem od lewej lub prawej krawędzi do środka ekranu" "Pamiętaj, aby przesuwać palcem od dolnej krawędzi ekranu" @@ -66,7 +68,6 @@ "Gest przechodzenia na ekran główny został opanowany" "Przesuń palcem, aby przejść na ekran główny" "Przesuń palcem od dołu ekranu. Ten gest zawsze powoduje przejście na ekran główny." - "Przesuń 2 palcami od dołu ekranu. Ten gest zawsze powoduje przejście do ekranu głównego." "Przejście na ekran główny" "Przesuń palcem z dołu ekranu w górę" "Brawo!" @@ -77,8 +78,7 @@ "Gest przełączania aplikacji został opanowany" "Przesuń palcem, aby przełączać aplikacje" "Aby przełączać się między aplikacjami, przesuń palcem od dołu do góry ekranu, przytrzymaj i puść." - "Aby przełączać się między aplikacjami, przesuń 2 palcami od dołu ekranu, przytrzymaj i puść." - "Przełącz aplikację" + "Przełączanie aplikacji" "Przesuń palcem z dołu ekranu w górę, przytrzymaj i puść" "Brawo!" "Wszystko gotowe" @@ -99,7 +99,7 @@ "Zapisz parę" "Aby podzielić ekran, kliknij drugą aplikację" "Aby podzielić ekran, wybierz drugą aplikację" - "Anuluj" + "Anuluj" "Wyjdź z wyboru podzielonego ekranu" "Wybierz drugą aplikację, aby podzielić ekran" "Nie możesz wykonać tego działania, bo nie zezwala na to aplikacja lub Twoja organizacja" @@ -130,18 +130,37 @@ "Szybkie ustawienia" "Pasek aplikacji" "Pasek aplikacji widoczny" - "Pasek aplikacji ukryty" + "Pasek i dymki po lewej" + "Pasek i dymki po prawej" "Pasek nawigacyjny" "Zawsze pokazuj pasek aplikacji" "Zmień tryb nawigacji" "Linia dzielenia paska aplikacji" + "Rozwijany pasek aplikacji" "Przesuń w górny lewy róg" "Przesuń w dolny prawy róg" - "{count,plural, =1{Pokaż jeszcze # aplikację.}few{Pokaż jeszcze # aplikacje.}many{Pokaż jeszcze # aplikacji.}other{Pokaż jeszcze # aplikacji.}}" - "{count,plural, =1{Pokaż # aplikację komputerową.}few{Pokaż # aplikacje komputerowe.}many{Pokaż # aplikacji komputerowych.}other{Pokaż # aplikacji komputerowej.}}" + "Otwórz aplikację jako dymek" + "Ostatnie aplikacje" + "Lista ostatnich aplikacji" + "{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}" + "Pulpit" "%1$s i %2$s" + "%1$s, element %2$d z %3$d" + "Przewiń w lewo" + "Przewiń w prawo" "Dymek" "Rozwijany" "%1$s z aplikacji %2$s" "%1$s i jeszcze %2$d" + "Przenieś w lewo" + "Przenieś w prawo" + "Zamknij wszystkie" + "rozwiń dymek: %1$s" + "zwiń dymek: %1$s" + "Zaznacz, aby wyszukać" + "Ikona aplikacji" + "Tytuł aplikacji" + "Przycisk Zamknij" + "Przypnij do paska" + "Odepnij od paska" diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml index e4d07bd97..2225496c1 100644 --- a/quickstep/res/values-pt-rPT/strings.xml +++ b/quickstep/res/values-pt-rPT/strings.xml @@ -22,11 +22,13 @@ "Fixar" "Forma livre" "Computador" + "Mover para o ecrã externo" + "Fechar" + "Computador" "Nenhum item recente" "Definições de utilização de aplicações" "Limpar tudo" "Apps recentes" - "Tarefa fechada" "%1$s, %2$s" "< 1 minuto" "Resta(m) %1$s hoje." @@ -45,9 +47,10 @@ "Sugestões de apps ativadas" "As sugestões de apps estão desativadas" "App prevista: %1$s" + "Tutorial da navegação por gestos" "Rode o dispositivo" "Rode o seu dispositivo para concluir o tutorial de navegação por gestos" - "Deslize rapidamente a partir da extremidade mais à direita ou mais à esquerda" + "Deslize a partir da extremidade mais à direita ou mais à esquerda" "Deslize rapidamente a partir da extremidade esquerda ou direita até ao centro do ecrã e solte" "Aprendeu a deslizar a partir da direita para retroceder. A seguir, saiba como alternar entre apps." "Concluiu o gesto para retroceder. A seguir, saiba como alternar entre apps." @@ -56,30 +59,27 @@ "Altere a sensibilidade do gesto para voltar nas Definições." "Deslize rapidamente com o dedo para retroceder" "Para voltar ao último ecrã, deslize rapidamente do limite esquerdo ou direito até ao centro do ecrã." - "Para voltar ao último ecrã, deslize rapidamente com 2 dedos a partir da extremidade esquerda ou direita até ao centro do ecrã." "Retroceder" - "Deslize rapidamente a partir da extremidade esquerda ou direita para o meio do ecrã" - "Deslize rapidamente com o dedo a partir do limite inferior do ecrã" + "Deslize a partir da extremidade esquerda ou direita até ao centro do ecrã" + "Deslize a partir do limite inferior do ecrã" "Não faça uma pausa antes de soltar" "Deslize rapidamente com o dedo para cima" "Concluiu o gesto para aceder ao ecrã principal. A seguir, saiba como retroceder." "Concluiu o gesto para aceder ao ecrã principal" "Deslize rapidamente com o dedo para aceder ao ecrã principal" "Deslize rapidamente para cima a partir da parte inferior. Este gesto abre sempre o ecrã principal." - "Deslize rapidamente para cima com 2 dedos no fundo do ecrã. Este gesto abre sempre o ecrã principal." "Aceda ao ecrã principal" - "Deslize rapidamente para cima a partir da parte inferior do ecrã" + "Deslize para cima a partir da parte inferior do ecrã" "Muito bem!" - "Deslize rapidamente com o dedo a partir do limite inferior do ecrã" + "Deslize a partir do limite inferior do ecrã" "Experimente premir a janela durante mais tempo antes de soltar" - "Garanta que desliza rapidamente com o dedo para cima e, em seguida, faz uma pausa" + "Deslize para cima e pause" "Aprendeu a utilizar gestos. Para desativar os gestos, aceda às Definições." "Concluiu o gesto para alternar entre apps" "Deslize rapidamente com o dedo para alternar entre apps" "Para alternar entre apps, deslize para cima sem soltar a partir da parte inferior do ecrã e solte." - "Para mudar de app, deslize rapidamente para cima com 2 dedos sem soltar no fundo do ecrã e solte." "Mude de app" - "Deslize rapidamente para cima a partir da parte inferior do ecrã sem soltar e, em seguida, solte" + "Deslize para cima a partir da parte inferior do ecrã, detenha o gesto e solte" "Muito bem!" "Está tudo pronto" "Concluído" @@ -99,7 +99,7 @@ "Guardar par de apps" "Toque noutra app para usar o ecrã dividido" "Escolha outra app para usar o ecrã dividido" - "Cancelar" + "Cancelar" "Saia da seleção de ecrã dividido" "Escolher outra app para usar o ecrã dividido" "Esta ação não é permitida pela app ou a sua entidade." @@ -130,18 +130,37 @@ "Definiç. rápidas" "Barra de tarefas" "Barra de tarefas apresentada" - "Barra de tarefas ocultada" + "Barra de tarefas/balões à esq." + "Barra de tarefas/balões à dir." "Barra de navegação" "Ver sempre Barra de tarefas" "Alterar modo de navegação" "Divisor da Barra de tarefas" + "Menu adicional da Barra de tarefas" "Mover para a parte superior esquerda" "Mover para a part superior direita" - "{count,plural, =1{Mostrar mais # app.}other{Mostrar mais # apps.}}" - "{count,plural, =1{Mostrar # app para computador.}other{Mostrar # apps para computador.}}" + "Abrir app como um balão" + "Apps recentes" + "Lista de apps recentes" + "{count,plural, =1{outra app}other{outras apps}}" + "Computador" "%1$s e %2$s" + "%1$s, item %2$d de %3$d" + "Deslocar para a esquerda" + "Deslocar para a direita" "Balão" "Menu adicional" "%1$s da app %2$s" "%1$s e mais %2$d pessoas" + "Mover para a esquerda" + "Mover para a direita" + "Ignorar tudo" + "expandir %1$s" + "reduzir %1$s" + "Circundar para Pesquisar" + "Ícone da app" + "Título da app" + "Botão Fechar" + "Afixar na barra tar." + "Desaf. da barra tar." diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml index 4fec4f851..e4c3cf199 100644 --- a/quickstep/res/values-pt/strings.xml +++ b/quickstep/res/values-pt/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fixar" "Forma livre" - "Computador" + "Modo área de trabalho" + "Mover para a tela externa" + "Fechar" + "Computador" "Nenhum item recente" "Configurações de uso do app" "Remover tudo" "Apps recentes" - "Tarefa encerrada" "%1$s, %2$s" "< 1 min" "%1$s restante(s) hoje" @@ -45,6 +47,7 @@ "O recurso \"sugestões de apps\" está ativado" "O recurso \"sugestões de apps\" está desativado" "App previsto: %1$s" + "Tutorial da navegação por gestos" "Gire o dispositivo" "Gire o dispositivo para concluir o tutorial da navegação por gestos" "Deslize da borda direita ou esquerda" @@ -56,7 +59,6 @@ "Mude a sensibilidade do gesto de voltar nas configurações" "Deslize para voltar" "Para voltar à tela anterior, deslize da borda esquerda ou direita até o meio da tela." - "Para voltar à tela anterior, deslize da borda esquerda ou direita até o meio da tela com dois dedos." "Volte" "Deslize da borda esquerda ou direita até o meio da tela" "Deslize da borda inferior da tela para cima" @@ -66,18 +68,16 @@ "Você concluiu o gesto para acessar a tela inicial" "Deslizar para voltar à tela inicial" "Deslize de baixo para cima na tela. Esse gesto sempre leva você para a tela inicial." - "Deslize de baixo para cima na tela com dois dedos. Esse gesto sempre leva você para a tela inicial." "Vá para a página inicial" "Deslize de baixo para cima na tela" "Muito bem!" "Deslize da borda inferior da tela para cima" "Mantenha a janela pressionada por mais tempo antes de soltar" - "Deslize para cima e pare" + "Deslize para cima em linha reta e pare" "Você aprendeu. Para desativar os gestos, acesse as Configurações." "Você concluiu o gesto para mudar de app" "Deslizar para trocar de app" "Para mudar de app, deslize de baixo para cima, mantenha a tela pressionada por um tempo e solte." - "Para mudar de app, deslize de baixo para cima na tela com dois dedos, segure por um tempo e solte." "Mude de app" "Deslize de baixo para cima na tela, segure e depois solte" "Muito bem!" @@ -89,8 +89,8 @@ "Tutorial %1$d/%2$d" "Tudo pronto!" "Deslize para cima para acessar a tela inicial" - "Toque no botão home para ir para a tela inicial" - "Você já pode começar a usar seu %1$s" + "Toque no botão home para acessar a tela inicial" + "O %1$s já pode ser usado" "dispositivo" "Configurações de navegação do sistema" "Compartilhar" @@ -99,7 +99,7 @@ "Salvar par de apps" "Toque em outro app para usar a tela dividida" "Escolha outro app para usar na tela dividida" - "Cancelar" + "Cancelar" "Sair da seleção de tela dividida" "Escolha outro app para usar na tela dividida" "Essa ação não é permitida pelo app ou pela organização" @@ -118,7 +118,7 @@ "Sempre mostrar a Barra de tarefas" "Toque e pressione o divisor para sempre mostrar a Barra de tarefas na parte de baixo da tela" "Toque na tecla de ação e pressione para pesquisar o que está na tela" - "O produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <a href="%1$s">Política de Privacidade</a> e aos <a href="%2$s">Termos de Serviço</a> do Google." + "Este produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <a href="%1$s">Política de Privacidade</a> e aos <a href="%2$s">Termos de Serviço</a> do Google." "Fechar" "Concluído" "Início" @@ -130,18 +130,37 @@ "Config. rápidas" "Barra de tarefas" "Barra de tarefas visível" - "Barra de tarefas oculta" + "Barra de tar. e balões à esq." + "Barra de tar. e balões à dir." "Barra de navegação" "Sempre mostrar a Barra de tarefas" "Mudar o modo de navegação" "Separador da Barra de tarefas" + "Barra de tarefas flutuante" "Mover para cima/para a esquerda" "Mover para baixo/para a direita" - "{count,plural, =1{Mostrar mais # app.}one{Mostrar mais # app.}other{Mostrar mais # apps.}}" - "{count,plural, =1{Mostrar # app para computador.}one{Mostrar # app para computador.}other{Mostrar # apps para computador.}}" + "Abrir o app como um balão" + "Apps recentes" + "Lista de apps recentes" + "{count,plural, =1{outro app}one{outro app}other{outros apps}}" + "Computador" "%1$s e %2$s" + "%1$s, item %2$d de %3$d" + "Rolar para a esquerda" + "Rolar para a direita" "Balão" "Balão flutuante" "%1$s do app %2$s" "%1$s e mais %2$d" + "Mover para esquerda" + "Mover para direita" + "Dispensar todos" + "abrir %1$s" + "fechar %1$s" + "Circule para pesquisar" + "Ícone do app" + "Título do app" + "Botão \"Fechar\"" + "Fixar na barra de tarefas" + "Liberar da barra de tarefas" diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml index c839602e8..27f80fbb5 100644 --- a/quickstep/res/values-ro/strings.xml +++ b/quickstep/res/values-ro/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fixează" "Formă liberă" - "Computer" + "Desktop" + "Mută pe ecranul extern" + "Închide" + "Computer" "Niciun element recent" "Setări de utilizare a aplicației" "Șterge tot" "Aplicații recente" - "Activitatea s-a încheiat" "%1$s, %2$s" "< 1 minut" "Au mai rămas %1$s astăzi" @@ -45,6 +47,7 @@ "Sugestiile de aplicații au fost activate" "Sugestiile de aplicații au fost dezactivate" "Aplicația estimată: %1$s" + "Tutorial de navigare prin gesturi" "Rotește dispozitivul" "Rotește dispozitivul pentru a încheia tutorialul de navigare prin gesturi" "Glisează dinspre marginea dreaptă îndepărtată sau dinspre marginea stângă îndepărtată" @@ -56,7 +59,6 @@ "Schimbă sensibilitatea gestului „Înapoi” accesând Setările" "Glisează pentru a reveni" "Pentru a reveni la ultimul ecran, glisează de la marginea stângă sau dreaptă spre mijlocul ecranului." - "Pentru a reveni la ultimul ecran, glisează cu două degete dinspre marginea stângă sau dreaptă spre mijlocul ecranului." "Înapoi" "Glisează dinspre marginea stângă sau dreaptă până la jumătatea ecranului" "Glisează în sus dinspre marginea de jos a ecranului" @@ -66,7 +68,6 @@ "Ai finalizat gestul „accesează ecranul de pornire”" "Glisează pentru a accesa ecranul de pornire" "Glisează în sus din partea de jos a ecranului. Cu acest gest accesezi mereu ecranul de pornire." - "Glisează în sus cu două degete din partea de jos. Cu acest gest accesezi mereu ecranul de pornire." "Înapoi la ecranul de pornire" "Glisează în sus din partea de jos a ecranului" "Excelent!" @@ -77,7 +78,6 @@ "Ai finalizat gestul „comută între aplicații”" "Glisează pentru a comuta între aplicații" "Ca să comuți între aplicații, glisează de jos în sus, ține degetul pe ecran, apoi ridică-l." - "Ca să comuți între aplicații, glisează cu 2 degete de jos în sus, ține-le pe ecran, apoi ridică-le." "Comută între aplicații" "Glisează în sus din partea de jos a ecranului, ține apăsat, apoi eliberează" "Felicitări!" @@ -99,7 +99,7 @@ "Salvează perechea de aplicații" "Atinge altă aplicație pentru ecranul împărțit" "Alege altă aplicație pentru a folosi ecranul împărțit" - "Anulează" + "Anulează" "Ieși din selecția cu ecran împărțit" "Alege altă aplicație pentru ecranul împărțit" "Această acțiune nu este permisă de aplicație sau de organizația ta" @@ -130,18 +130,37 @@ "Setări rapide" "Bară de activități" "Bara de activități este afișată" - "Bara de activități este ascunsă" + "Bară și baloane stânga afișate" + "Bară & baloane dreapta afișate" "Bară de navigare" "Afișează mereu bara" "Schimbă modul de navigare" "Separator pentru bara de activități" + "Meniu suplimentar pentru bara de activități" "Mută în stânga sus" "Mută în dreapta jos" - "{count,plural, =1{Afișează încă # aplicație}few{Afișează încă # aplicații}other{Afișează încă # de aplicații}}" - "{count,plural, =1{Afișează # aplicație pentru computer.}few{Afișează # aplicații pentru computer.}other{Afișează # de aplicații pentru computer.}}" + "Deschide aplicația ca balon" + "Aplicații recente" + "Lista de aplicații recente" + "{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}" + "Computer" "%1$s și %2$s" + "%1$s, articolul %2$d din %3$d" + "Derulează la stânga" + "Derulează la dreapta" "Balon" "Suplimentar" "%1$s de la %2$s" "%1$s și încă %2$d" + "Deplasează spre stânga" + "Deplasează spre dreapta" + "Închide-le pe toate" + "extinde %1$s" + "restrânge %1$s" + "Încercuiește și caută" + "Pictograma aplicației" + "Titlul aplicației" + "Buton de închidere" + "Fixează pe bara de activități" + "Anulează fixarea din bara de activități" diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml index da49ad32b..605c5654e 100644 --- a/quickstep/res/values-ru/strings.xml +++ b/quickstep/res/values-ru/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Закрепить" "Произвольная форма" - "Включить режим для ПК" + "Мультиоконный режим" + "Перенести на внешний дисплей" + "Закрыть" + "Мультиоконный режим" "Здесь пока ничего нет." "Настройки использования приложения" "Очистить все" "Недавние приложения" - "Задача закрыта" "%1$s: %2$s" "< 1 мин." "Осталось сегодня: %1$s" @@ -45,6 +47,7 @@ "Функция \"Рекомендуемые приложения\" включена." "Функция \"Рекомендуемые приложения\" отключена." "Рекомендуемое приложение: %1$s" + "Руководство: навигация с помощью жестов" "Поверните устройство" "Чтобы перейти к руководству по жестам, нужно повернуть устройство." "Проведите справа налево или слева направо от самого края экрана." @@ -56,7 +59,6 @@ "Уровень чувствительности можно изменить в настройках." "Возврат к предыдущему экрану" "Чтобы вернуться к предыдущему экрану, проведите от левого или правого края дисплея к центру." - "Чтобы вернуться на предыдущий экран, проведите двумя пальцами от левого или правого края экрана к центру." "Как вернуться к предыдущему экрану" "Проведите от левого или правого края экрана к центру." "Проведите снизу вверх от самого края экрана." @@ -66,18 +68,16 @@ "Вы выполнили жест для перехода на главный экран." "Переход на главный экран" "Проведите вверх от нижнего края дисплея. Этот жест открывает главный экран." - "Проведите двумя пальцами вверх от нижнего края экрана. Этот жест открывает главный экран." "Как перейти на главный экран" "Проведите вверх от нижнего края экрана." "У вас получилось!" "Проведите снизу вверх от самого края экрана." "Прежде чем отпустить палец, задержите его на экране немного дольше." - "Проведите по экрану ровно вверх и задержите палец в конце." + "Проведите по экрану вверх и задержите палец." "Теперь вы знаете, как использовать жесты. Чтобы отключить их, перейдите в настройки." "Вы выполнили жест для переключения между приложениями." "Переключение между приложениями" "Чтобы переключиться между приложениями‚ проведите по экрану снизу вверх, задержите палец, а затем отпустите." - "Чтобы сменить приложение, проведите двумя пальцами снизу вверх, задержите пальцы, а затем отпустите." "Как переключаться между приложениями" "Проведите вверх от нижнего края экрана, задержите палец в одной точке и отпустите." "Отлично!" @@ -92,14 +92,14 @@ "Нажмите кнопку главного экрана, чтобы открыть его." "Теперь вы можете использовать %1$s." "устройство" - "Системные настройки навигации" + "Настройки навигации в системе" "Поделиться" "Скриншот" "Разделить" "Сохранить приложения" "Для разделения экрана выберите другое приложение." "Чтобы использовать разделенный экран, выберите другое приложение." - "Отмена" + "Отмена" "Выйдите из режима разделения экрана." "Выберите другое приложение для разделения экрана." "Это действие заблокировано приложением или организацией." @@ -130,18 +130,37 @@ "Быстрые настройки" "Панель задач" "Панель задач показана" - "Панель задач скрыта" + "Слева панель задач, подсказки" + "Справа панель задач, подсказки" "Панель навигации" "Всегда показывать панель задач" "Изменить режим навигации" "Разделитель панели задач" + "Дополнительное меню панели задач" "Переместить вверх или влево" "Переместить вниз или вправо" - "{count,plural, =1{Показать ещё # приложение}one{Показать ещё # приложение}few{Показать ещё # приложения}many{Показать ещё # приложений}other{Показать ещё # приложения}}" - "{count,plural, =1{Показать # компьютерное приложение.}one{Показать # компьютерное приложение.}few{Показать # компьютерных приложения.}many{Показать # компьютерных приложений.}other{Показать # компьютерного приложения.}}" + "Открыть приложение во всплывающем окне" + "Недавние приложения" + "Список недавних приложений" + "{count,plural, =1{дополнительное приложение}one{дополнительное приложение}few{дополнительных приложения}many{дополнительных приложений}other{дополнительного приложения}}" + "Режим компьютера" "%1$s и %2$s" + "%1$s, элемент %2$d из %3$d" + "Прокрутить влево" + "Прокрутить вправо" "Всплывающая подсказка" "Дополнительное меню" "\"%1$s\" из приложения \"%2$s\"" "%1$s и ещё %2$d" + "Переместить влево" + "Переместить вправо" + "Закрыть все" + "Развернуто: %1$s" + "Свернуто: %1$s" + "Обвести и найти" + "Значок приложения" + "Название приложения" + "Кнопка \"Закрыть\"" + "Закрепить на панели" + "Открепить от панели" diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml index 9cbe837f9..2030252b2 100644 --- a/quickstep/res/values-si/strings.xml +++ b/quickstep/res/values-si/strings.xml @@ -22,11 +22,13 @@ "අමුණන්න" "Freeform" "ඩෙස්ක්ටොපය" + "බාහිර සංදර්ශකය වෙත ගෙන යන්න" + "වසන්න" + "ඩෙස්ක්ටොපය" "මෑත අයිතම නැත" "යෙදුම් භාවිත සැකසීම්" "සියල්ල හිස් කරන්න" "මෑත යෙදුම්" - "කාර්යය අවසන් කරන ලදි" "%1$s, %2$s" "< 1 විනාඩියක්" "අද %1$sක් ඉතුරුයි" @@ -45,6 +47,7 @@ "යෙදුම් යෝජනා සබලිතයි" "යෙදුම් යෝජනා අබල කර ඇත" "පුරෝකථනය කළ යෙදුම: %1$s" + "අභින සංචාලන නිබන්ධනය" "ඔබේ උපාංගය කරකවන්න" "අභින සංචාලන නිබන්ධනය සම්පූර්ණ කිරීම සඳහා ඔබේ උපාංගය කරකවන්න" "ඔබ ඈත දකුණු හෝ ඈත වම් දාරයේ සිට ස්වයිප් කරන බව සහතික කර ගන්න" @@ -56,7 +59,6 @@ "ආපසු ඉංගිතයෙහි සංවේදීතාව වෙනස් කිරීමට, සැකසීම් වෙත යන්න" "ආපසු යාමට ස්වයිප් කරන්න" "අවසාන තිරයට ආපසු යාමට, වම් හෝ දකුණු දාරයෙන් තිරයේ මැදට ස්වයිප් කරන්න." - "අවසාන තිරයට ආපසු යාමට, වම් හෝ දකුණු දාරයෙන් තිරයේ මැදට ඇඟිලි 2කින් ස්වයිප් කරන්න." "ආපසු යන්න" "වම් හෝ දකුණු කෙළවරේ සිට තිරයේ මැදට ස්වයිප් කරන්න" "ඔබ තිරයේ පහළ දාරයේ සිට ඉහළට ස්වයිප් කරන බව සහතික කර ගන්න" @@ -66,7 +68,6 @@ "ඔබ මුල් පිටුවට යාමේ ඉංගිතය සම්පූර්ණ කළා" "මුල් පිටුවට යාමට ස්වයිප් කරන්න" "ඔබගේ තිරයේ පහළින් උඩට ස්වයිප් කරන්න.මෙම ඉංගිතය සැම විටම ඔබව මුල් තිරයට ගෙන යයි." - "තිරයේ පහළම සිට ඇඟිලි 2කින් ඉහළට ස්වයිප් කරන්න. මෙම ඉංගිතය සැම විටම ඔබව මුල් තිරයට ගෙන යයි." "මුල් පිටුවට යන්න" "ඔබේ තිරයේ පහළ සිට උඩට ස්වයිප් කරන්න" "අනර්ඝ වැඩක්!" @@ -77,7 +78,6 @@ "ඔබ යෙදුම් මාරු කිරීමේ ඉංගිතය සම්පූර්ණ කළා" "යෙදුම් මාරු කිරීමට ස්වයිප් කරන්න" "යෙදුම් අතර මාරු වීමට, ඔබගේ තිරයේ පහළම සිට උඩට ස්වයිප් කර, අල්ලාගෙන සිට, අනතුරුව මුදා හරින්න." - "යෙදුම් අතර මාරු වීමට, ඔබගේ තිරයේ පහළම සිට උඩට ඇඟිලි 2කින් ස්වයිප් කර, අල්ලාගෙන සිට, අනතුරුව මුදා හරින්න." "යෙදුම් මාරු කරන්න" "ඔබේ තිරයේ පහළ සිට ඉහළට ස්වයිප් කරන්න, රඳවා ගෙන සිට, පසුව මුදා හරින්න" "හොඳින් කළා!" @@ -99,7 +99,7 @@ "යෙදුම් යුගල සුරකින්න" "බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න" "බෙදුම් තිරය භාවිත කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" - "අවලංගු කරන්න" + "අවලංගු කරන්න" "බෙදීම් තිර තේරීමෙන් පිටවන්න" "බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" "මෙම ක්‍රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ" @@ -130,18 +130,37 @@ "ඉක්මන් සැකසීම්" "කාර්ය තීරුව" "කාර්ය තීරුව පෙන්වා ඇත" - "කාර්ය තීරුව සඟවා ඇත" + "කාර්ය තීරුව සහ බුබුළු පෙන්වා ඇත" + "කාර්ය තීරුව සහ බුබුළු දකුණට පෙන්වා ඇත" "සංචලන තීරුව" "සෑම විටම කාර්ය තීරුව පෙන්වන්න" "සංචාලන ප්‍රකාරය වෙනස් කරන්න" "කාර්ය තීරු බෙදනය" + "කාර්ය තීරුව පිටාර යාම" "ඉහළ/වම වෙත ගෙන යන්න" "පහළ/දකුණ වෙත ගෙන යන්න" - "{count,plural, =1{තවත් # යෙදුමක් පෙන්වන්න.}one{තවත් යෙදුම් #ක් පෙන්වන්න.}other{තවත් යෙදුම් #ක් පෙන්වන්න.}}" - "{count,plural, =1{# ඩෙස්ක්ටොප් යෙදුමක් පෙන්වන්න.}one{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}other{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}}" + "යෙදුම බුබුලක් ලෙස විවෘත කරන්න" + "මෑත යෙදුම්" + "මෑත යෙදුම් ලැයිස්තුව" + "{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}" + "ඩෙස්ක්ටොපය" "%1$s සහ %2$s" + "%1$s, අයිතම %3$dන් %2$d" + "වමට අනුචලනය කරන්න" + "දකුණට අනුචලනය කරන්න" "බුබුළු" "පිටාර යාම" "%2$s සිට %1$s" "%1$s හා තව %2$dක්" + "වමට ගෙන යන්න" + "දකුණට ගෙන යන්න" + "සියල්ල ඉවතලන්න" + "%1$s දිග හරින්න" + "%1$s හකුළන්න" + "සෙවීමට කවයසෙවීමට කවය අදින්න" + "යෙදුම් නිරූපකය" + "යෙදුම් මාතෘකාව" + "වැසීමේ බොත්තම" + "කාර්ය තීරුවට අමුණන්න" + "කාර්ය තීරුවෙන් ඉවත් කරන්න" diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml index 3eca787fe..7e071167a 100644 --- a/quickstep/res/values-sk/strings.xml +++ b/quickstep/res/values-sk/strings.xml @@ -22,11 +22,13 @@ "Pripnúť" "Voľný režim" "Počítač" + "Presunúť na externú obrazovku" + "Zavrieť" + "Počítač" "Žiadne nedávne položky" "Nastavenia využívania aplikácie" "Vymazať všetko" "Nedávne aplikácie" - "Úloha bola zavretá" "%1$s, %2$s" "Menej ako 1 minúta" "Dnes ešte zostáva: %1$s" @@ -45,41 +47,39 @@ "Návrhy aplikácií zapnuté" "Návrhy aplikácií vypnuté" "Predpovedaná aplikácia: %1$s" + "Návod na navigáciu gestami" "Otočte zariadenie" "Otočte zariadenie a dokončite tak návod, ako navigovať gestami" - "Musíte potiahnuť úplne z pravého alebo ľavého okraja" + "Musíte potiahnuť úplne z pravého alebo ľavého okraja." "Musíte potiahnuť z pravého alebo ľavého okraja do stredu obrazovky a potom uvoľniť" "Naučili ste sa prejsť späť potiahnutím sprava. V ďalšom kroku sa naučíte prepínať aplikácie." "Dokončili ste gesto na prechod späť. V ďalšom kroku sa naučíte, ako prepínať aplikácie." - "Dokončili ste gesto na prechod späť" + "Použili ste gesto na prechod späť." "Nesmiete potiahnuť príliš blízko dolnej časti obrazovky" "Ak chcete zmeniť citlivosť gesta Späť, prejdite do Nastavení" "Prechod späť potiahnutím" "Na poslednú obrazovku prejdete potiahnutím z ľavého alebo pravého okraja do stredu obrazovky." - "Na poslednú obrazovku sa vrátite potiahnutím dvoma prstami z ľavého alebo pravého okraja do stredu obrazovky." "Prechod späť" - "Potiahnite z ľavého alebo pravého okraja do stredu obrazovky" - "Musíte potiahnuť nahor z dolného okraja obrazovky" + "Potiahnite z ľavého alebo pravého okraja do stredu obrazovky." + "Musíte potiahnuť nahor z dolného okraja obrazovky." "Pred uvoľnením nesmiete zastať" "Musíte potiahnuť priamo nahor" "Dokončili ste gesto prechodu na plochu. Teraz sa naučíte, ako sa vrátiť späť." "Dokončili ste gesto prechodu na plochu" "Prechod na plochu potiahnutím" "Potiahnite nahor zdola obrazovky. Týmto gestom sa vždy vrátite na plochu." - "Postiahnite dvoma prstami z dolnej časti obrazovky. Týmto gestom sa vždy vrátite na plochu." "Prechod na plochu" - "Potiahnite z dolnej časti obrazovky nahor" + "Potiahnite z dolnej časti obrazovky nahor." "Skvelé!" "Musíte potiahnuť nahor z dolného okraja obrazovky" "Skúste okno pred uvoľnením podržať dlhšie" - "Musite potiahnuť priamo nahor a potom zastať" - "Naučili ste sa používať gestá. Gestá môžete vypnúť v nastaveniach." - "Dokončili ste gesto na prepnutie aplikácií" + "Musite potiahnuť priamo nahor a potom zastať." + "Naučili ste sa používať gestá. Gestá môžete vypnúť v Nastaveniach." + "Použili ste gesto na prepnutie aplikácií." "Prepínanie aplikácií potiahnutím" "Aplikácie môžete prepínať potiahnutím obrazovky zdola nahor, pridržaním a následným uvoľnením." - "Aplikácie prepnete potiahnutím dvoma prstami z dolnej časti obrazovky, ich pridržaním a uvoľnením." "Prepnutie aplikácií" - "Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite" + "Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite." "Výborne" "Hotovo" "Hotovo" @@ -90,7 +90,7 @@ "Hotovo" "Potiahnutím nahor prejdete na plochu" "Na plochu prejdete klepnutím na tlačidlo plochy" - "%1$s môžete začať používať" + "Môžete %1$s začať používať" "zariadenie" "Nastavenia navigácie systémom" "Zdieľať" @@ -99,7 +99,7 @@ "Uložiť pár aplikácií" "Obrazovku rozdelíte klepnutím na inú aplikáciu" "Na použitie rozdelenej obrazovky vyberte ďalšiu aplikáciu" - "Zrušiť" + "Zrušiť" "Ukončite výber rozdelenej obrazovky" "Na použitie rozd. obrazovky vyberte inú aplikáciu" "Aplikácia alebo vaša organizácia túto akciu nepovoľuje" @@ -130,18 +130,37 @@ "Rýchle nastavenia" "Panel aplikácií" "Panel aplikácií je zobrazený" - "Panel aplikácií je skrytý" + "Panel aplik. a bubl. sú vľavo" + "Panel aplik. a bubl. sú vpravo" "Navigačný panel" "Zobrazovať panel aplikácií" "Zmeniť režim navigácie" "Rozdeľovač panela aplikácií" + "Rozšírená ponuka panela aplikácií" "Presunúť hore alebo doľava" "Presunúť dole alebo doprava" - "{count,plural, =1{Zobraziť # ďalšiu aplikáciu.}few{Zobraziť # ďalšie aplikácie.}many{Show # more apps.}other{Zobraziť # ďalších aplikácií.}}" - "{count,plural, =1{Zobraziť # aplikáciu pre počítač.}few{Zobraziť # aplikácie pre počítač.}many{Show # desktop apps.}other{Zobraziť # aplikácií pre počítač.}}" + "Otvoriť aplikáciu ako bublinu" + "Nedávne aplikácie" + "Zoznam nedávnych aplikácií" + "{count,plural, =1{ďalšia aplikácia}few{ďalšie aplikácie}many{ďalšie aplikácie}other{ďalšie aplikácie}}" + "Počítač" "%1$s a %2$s" + "%1$s, %2$d. položka z %3$d" + "Posunúť doľava" + "Posunúť doprava" "Bublina" "Rozbaľovacia ponuka" "%1$s z aplikácie %2$s" "%1$s a ešte %2$d" + "Posunúť doľava" + "Posunúť doprava" + "Zavrieť všetko" + "rozbaliť %1$s" + "zbaliť %1$s" + "Vyhľadávanie krúžením" + "Ikona aplikácie" + "Názov aplikácie" + "Tlačidlo Zavrieť" + "Pripnúť na panel" + "Odopnúť z panela" diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml index 52faeb7a9..fd7dc69d4 100644 --- a/quickstep/res/values-sl/strings.xml +++ b/quickstep/res/values-sl/strings.xml @@ -22,11 +22,13 @@ "Pripni" "Prosta oblika" "Namizni računalnik" + "Premik v zunanji zaslon" + "Zapri" + "Namizni način" "Ni nedavnih elementov" "Nastavitve uporabe aplikacij" "Počisti vse" "Nedavne aplikacije" - "Opravilo je zaprto" "%1$s, %2$s" "< 1 min" "Danes je ostalo še %1$s" @@ -45,6 +47,7 @@ "Predlogi aplikacij so omogočeni." "Predlogi aplikacij so onemogočeni." "Predvidena aplikacija: %1$s" + "Vadnica za krmarjenje s potezami" "Zasukajte napravo" "Zasukajte napravo, če si želite ogledati vadnico za krmarjenje s potezami" "Pazite, da povlečete s skrajno desnega ali skrajno levega roba." @@ -56,7 +59,6 @@ "Občutljivost poteze za nazaj lahko spremenite v nastavitvah." "Povlecite za vrnitev" "Če se želite vrniti na prejšnji zaslon, povlecite z levega ali desnega roba do sredine zaslona." - "Če se želite vrniti na zadnji prikazani zaslon, z dvema prstoma povlecite z levega ali desnega roba do sredine zaslona." "Pomik nazaj" "Povlecite z levega ali desnega roba do sredine zaslona." "Pazite, da povlečete s spodnjega roba zaslona navzgor." @@ -66,7 +68,6 @@ "Izvedli ste potezo za pomik na začetni zaslon." "Povlecite za pomik na začetni zaslon" "Z dna zaslona s prstom povlecite navzgor. S to potezo lahko vedno odprete začetni zaslon." - "Z dvema prstoma povlecite navzgor z dna zaslona. S to potezo lahko vedno odprete začetni zaslon." "Pomik na začetni zaslon" "Z dna zaslona s prstom povlecite navzgor." "Odlično!" @@ -77,7 +78,6 @@ "Izvedli ste potezo za preklapljanje med aplikacijami." "Povlecite za preklapljanje med aplikacijami" "Za preklapljanje med aplikacijami povlecite navzgor z dna zaslona, pridržite in nato izpustite." - "Za preklop med aplikacijami z dvema prstoma povlecite navzgor z dna zaslona, pridržite in spustite." "Preklop aplikacij" "Povlecite navzgor z dna zaslona, pridržite, nato izpustite." "Odlično!" @@ -99,7 +99,7 @@ "Shrani par aplikacij" "Za razdeljeni zaslon se dotaknite še 1 aplikacije" "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." - "Prekliči" + "Prekliči" "Zapri izbiro razdeljenega zaslona" "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." "Aplikacija ali vaša organizacija ne dovoljuje tega dejanja" @@ -130,18 +130,37 @@ "Hitre nastavitve" "Opravilna vrstica" "Opravilna vrstica je prikazana" - "Opravilna vrstica je skrita" + "Prikazani so opravilna vrstica in oblački na levi" + "Prikazani so opravilna vrstica in oblački na desni" "Vrstica za krmarjenje" "Stalen prikaz oprav. vrstice" "Spreminjanje načina navigacije" "Razdelilnik opravilne vrstice" + "Oblaček opravilne vrstice z dodatnimi elementi" "Premakni na vrh/levo" "Premakni na dno/desno" - "{count,plural, =1{Pokaži še # aplikacijo.}one{Pokaži še # aplikacijo.}two{Pokaži še # aplikaciji.}few{Pokaži še # aplikacije.}other{Pokaži še # aplikacij.}}" - "{count,plural, =1{Prikaz # aplikacije za namizni računalnik.}one{Prikaz # aplikacije za namizni računalnik.}two{Prikaz # aplikacij za namizni računalnik.}few{Prikaz # aplikacij za namizni računalnik.}other{Prikaz # aplikacij za namizni računalnik.}}" + "Odpri aplikacijo kot oblaček" + "Nedavne aplikacije" + "Seznam nedavnih aplikacij" + "{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}two{dodatni aplikaciji}few{dodatne aplikacije}other{dodatnih aplikacij}}" + "Namizni računalnik" "%1$s in %2$s" + "%1$s, element %2$d od %3$d" + "Pomik levo" + "Pomik desno" "Oblaček" "Oblaček z dodatnimi elementi" "%1$s iz aplikacije %2$s" "%1$s in še %2$d" + "Premik v levo" + "Premik v desno" + "Opusti vse" + "razširitev oblačka %1$s" + "strnitev oblačka %1$s" + "Iskanje z obkroževanjem" + "Ikona aplikacije" + "Ime aplikacije" + "Gumb za zapiranje" + "Pripni v opravilno vrstico" + "Odpni iz opravilne vrstice" diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml index cdb9cf9a7..3ebff3a74 100644 --- a/quickstep/res/values-sq/strings.xml +++ b/quickstep/res/values-sq/strings.xml @@ -22,11 +22,13 @@ "Gozhdo" "Formë e lirë" "Desktopi" + "Zhvendose tek ekrani i jashtëm" + "Mbyll" + "Desktopi" "Nuk ka asnjë artikull të fundit" "Cilësimet e përdorimit të aplikacionit" "Pastroji të gjitha" "Aplikacionet e fundit" - "Detyra u mbyll" "%1$s, %2$s" "< 1 minutë" "%1$s të mbetura sot" @@ -45,6 +47,7 @@ "Aplikacionet e sugjeruara janë aktivizuar" "Sugjerimet e aplikacioneve janë çaktivizuar" "Aplikacioni i parashikuar: %1$s" + "Udhëzuesi për navigimin me gjeste" "Rrotullo pajisjen" "Rrotullo pajisjen për të përfunduar udhëzuesin e navigimit me gjeste" "Sigurohu që të rrëshqasësh shpejt nga skaji më i djathtë ose më i majtë" @@ -56,7 +59,6 @@ "Për të ndryshuar ndjeshmërinë e gjestit të kthimit prapa, shko te \"Cilësimet\"" "Rrëshqit shpejt për t\'u kthyer prapa" "Për t\'u kthyer prapa tek ekrani i fundit, rrëshqit shpejt nga skaji i majtë ose i djathtë drejt mesit të ekranit" - "Për t\'u kthyer prapa tek ekrani i fundit, rrëshqit shpejt me 2 gishta nga skaji i majtë ose i djathtë drejt mesit të ekranit." "Kthehu prapa" "Rrëshqit shpejt nga skaji i majtë ose i djathtë drejt mesit të ekranit" "Sigurohu që të rrëshqasësh shpejt lart nga skaji i poshtëm i ekranit" @@ -66,7 +68,6 @@ "E ke përfunduar gjestin e kalimit tek ekrani bazë" "Rrëshqit shpejt për të kaluar tek ekrani bazë" "Rrëshqit shpejt lart nga fundi i ekranit tënd. Ky gjest të dërgon gjithmonë tek ekrani bazë." - "Rrëshqit shpejt lart me 2 gishta nga fundi i ekranit. Ky gjest të dërgon gjithmonë tek ekrani bazë." "Shko tek ekrani bazë" "Rrëshqit shpejt lart nga pjesa e poshtme e ekranit" "Punë e shkëlqyer!" @@ -77,7 +78,6 @@ "E ke përfunduar gjestin e ndërrimit të aplikacioneve" "Rrëshqit shpejt për të ndërruar aplikacionet" "Për të ndërruar mes aplikacioneve, rrëshqit shpejt lart nga fundi i ekranit tënd, mbaj dhe pastaj lësho." - "Për të ndërruar mes aplikacioneve, rrëshqit lart me 2 gishta nga fundi i ekranit, mbaje dhe lëshoje." "Ndërro aplikacionet" "Rrëshqit shpejt lart nga fundi i ekranit, mbaje të shtypur dhe më pas lëshoje" "Shumë mirë!" @@ -99,7 +99,7 @@ "Ruaj çiftin e aplikacioneve" "Trokit një apl. tjetër; përdor ekranin e ndarë" "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" - "Anulo" + "Anulo" "Dil nga zgjedhja e ekranit të ndarë" "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" "Ky veprim nuk lejohet nga aplikacioni ose organizata jote" @@ -130,18 +130,37 @@ "Cilësimet shpejt" "Shiriti i detyrave" "Shiriti i detyrave i shfaqur" - "Shiriti i detyrave i fshehur" + "Shiriti i detyrave dhe flluskat majtas janë shfaqur" + "Shiriti i detyrave dhe flluskat djathtas janë shfaqur" "Shiriti i navigimit" "Shfaq gjithmonë shiritin e detyrave" "Ndrysho modalitetin e navigimit" "Ndarësi i shiritit të detyrave" + "Tejkalimi i shiritit të detyrave" "Lëviz në krye/majtas" "Lëviz në fund/djathtas" - "{count,plural, =1{Shfaq # aplikacion tjetër.}other{Shfaq # aplikacione të tjera.}}" - "{count,plural, =1{Shfaq # aplikacion për desktop.}other{Shfaq # aplikacione për desktop.}}" + "Hap aplikacionin si një flluskë" + "Aplikacionet e fundit" + "Lista e aplikacioneve të fundit" + "{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}" + "Desktop" "%1$s dhe %2$s" + "%1$s, artikulli %2$d nga %3$d" + "Lëviz majtas" + "Lëviz djathtas" "Flluska" "Tejkalimi" "\"%1$s\" nga %2$s" "\"%1$s\" dhe %2$d të tjera" + "Lëviz majtas" + "Lëviz djathtas" + "Hiqi të gjitha" + "zgjero %1$s" + "palos %1$s" + "Qarko për të kërkuar" + "Ikona e aplikacionit" + "Titulli i aplikacionit" + "Butoni i mbylljes" + "Gozhdo te shiriti" + "Zhgozhdo nga shiriti" diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml index 7456a36b7..6742da996 100644 --- a/quickstep/res/values-sr/strings.xml +++ b/quickstep/res/values-sr/strings.xml @@ -22,11 +22,13 @@ "Закачи" "Слободни облик" "Рачунар" + "Преместите на спољни екран" + "Затвори" + "Рачунари" "Нема недавних ставки" "Подешавања коришћења апликације" "Обриши све" "Недавне апликације" - "Задатак је затворен" "%1$s, %2$s" "< 1 мин" "Још %1$s данас" @@ -45,18 +47,18 @@ "Предлози апликација су омогућени" "Предлози апликација су онемогућени" "Предвиђамо апликацију: %1$s" + "Водич за навигацију помоћу покрета" "Ротирајте уређај" "Ротирајте уређај да бисте довршили водич за навигацију помоћу покрета" "Обавезно превуците од саме десне или леве ивице" "Обавезно превуците од десне или леве ивице до средине екрана и отпустите" "Научили сте како да превлачите здесна да бисте се вратили уназад. Сада научите да замените апликације." "Довршили сте покрет за повратак. Сада сазнајте како да промените апликације." - "Довршили сте покрет за повратак" + "Довршили сте покрет за назад" "Никако не превлачите превише близу дна екрана" "Осетљивост пок. за назад можете да промените у Подешавањима" "Превуците да бисте се вратили уназад" "Да бисте се вратили на последњи екран, превуците од леве или десне ивице до средине екрана." - "Да бисте се вратили на последњи екран, превуците помоћу два прста од леве или десне ивице до средине екрана." "Назад" "Превуците од леве или десне ивице до средине екрана" "Обавезно превуците нагоре од доње ивице екрана" @@ -66,8 +68,7 @@ "Довршили сте покрет за повратак на почетну страницу." "Превуците да бисте отишли на почетну страницу" "Превуците нагоре од дна екрана. Овај покрет вас увек води на почетни екран." - "Превуците помоћу два прста нагоре од дна екрана. Овим покретом увек отварате почетни екран." - "Идите на почетни екран" + "На почетни екран" "Превуците нагоре са доњег дела екрана" "Одлично!" "Обавезно превуците нагоре од доње ивице екрана" @@ -77,8 +78,7 @@ "Довршили сте покрет за промену апликација" "Превуците да бисте заменили апликације" "За прелазак са једне апликације на другу превуците нагоре од дна екрана, задржите, па пустите." - "За прелазак између апликација превуците помоћу два прста нагоре од дна екрана, задржите, па пустите." - "Пређите на другу апликацију" + "На другу апликацију" "Превуците нагоре од дна екрана, задржите, па пустите" "Одлично!" "То је то" @@ -89,7 +89,7 @@ "Водич %1$d/%2$d" "Готово!" "Превуците нагоре да бисте отворили почетни екран" - "Додирните дугме Почетак да бисти ишли на почетни екран" + "Додирните дугме Почетак да бисте отишли на почетни екран" "Спремни сте да почнете да користите %1$s" "уређај" "Подешавања кретања кроз систем" @@ -99,7 +99,7 @@ "Сачувај пар апликација" "Додирните другу апликацију за подељени екран" "Одаберите другу апликацију да бисте користили подељени екран" - "Откажи" + "Откажи" "Излазак из бирања подељеног екрана" "Одаберите другу апликацију за подељени екран" "Апликација или организација не дозвољавају ову радњу" @@ -130,18 +130,37 @@ "Брза подешавања" "Трака задатака" "Трака задатака је приказана" - "Трака задатака је скривена" + "Приказ задатака/облачића лево" + "Приказ задатака/облачића десно" "Трака за навигацију" "Увек приказуј траку задатака" "Промени режим навигације" "Разделник траке задатака" + "Преклопна трака задатака" "Премести горе лево" "Премести доле десно" - "{count,plural, =1{Прикажи још # апликацију.}one{Прикажи још # апликацију.}few{Прикажи још # апликације.}other{Прикажи још # апликација.}}" - "{count,plural, =1{Прикажи # апликацију за рачунаре.}one{Прикажи # апликацију за рачунаре.}few{Прикажи # апликације за рачунаре.}other{Прикажи # апликација за рачунаре.}}" + "Отвори апликацију као облачић" + "Недавне апликације" + "Листа недавних апликација" + "{count,plural, =1{додатна апликација}one{додатна апликација}few{додатне апликације}other{додатних апликација}}" + "Рачунар" "%1$s и %2$s" + "%1$s, ставка %2$d од %3$d" + "Скролујте улево" + "Скролујте удесно" "Облачић" "Преклопни" "%1$s – %2$s" "%1$s и још %2$d" + "Помери налево" + "Помери надесно" + "Одбаци све" + "проширите облачић %1$s" + "скупите облачић %1$s" + "Претрага заокруживањем" + "Икона апликације" + "Назив апликације" + "Дугме Затвори" + "Закачи за траку зад." + "Откачи са траке зад." diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml index f369daeb3..c995fd052 100644 --- a/quickstep/res/values-sv/strings.xml +++ b/quickstep/res/values-sv/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fäst" "Fritt format" - "Dator" + "Skrivbordsläge" + "Flytta till extern skärm" + "Stäng" + "Skrivbordsläge" "Listan är tom" "Inställningar för appanvändning" "Rensa alla" "Senaste apparna" - "Uppgiften har stängts" "%1$s, %2$s" "< 1 min" "%1$s kvar i dag" @@ -45,6 +47,7 @@ "Appförslag har aktiverats" "Appförslag har inaktiverats" "Appförslag: %1$s" + "Guide för navigering med rörelser" "Rotera enheten" "Rotera enheten för att slutföra guiden för navigering med rörelser" "Se till att du sveper ända från högerkanten eller vänsterkanten" @@ -56,7 +59,6 @@ "Öppna inställningarna om du vill ändra rörelsens känslighet" "Svep för att återgå" "Återgå till den senaste skärmen genom att svepa från skärmens vänster- eller högerkant till mitten." - "Gå tillbaka till den senaste skärmen genom att med två fingrar svepa mot mitten av skärmen från vänster eller höger kant." "Tillbaka" "Svep från den högra eller vänstra kanten till mitten av skärmen" "Se till att du sveper uppåt från nederkanten av skärmen" @@ -66,7 +68,6 @@ "Du är klar med rörelsen för att öppna startskärmen" "Svep för att öppna startskärmen" "Svep uppåt från skärmens nederkant. Du kan alltid återgå till startskärmen med den här rörelsen." - "Svep uppåt med två fingrar från skärmens nederkant. Så kommer du alltid tillbaka till startskärmen." "Öppna startskärmen" "Svep uppåt från skärmens nederkant" "Bra jobbat!" @@ -77,7 +78,6 @@ "Du är klar med rörelsen för att byta mellan appar" "Svep för att byta mellan appar" "Byt mellan appar genom att svepa uppåt från skärmens nederkant. Håll fingret nedtryckt och släpp." - "Byta mellan appar: Svep uppåt med två fingrar från skärmens nederkant, håll kvar och släpp sedan." "Byt app" "Svep uppåt från skärmens nederkant. Håll fingret nedtryckt och släpp sedan" "Bra gjort!" @@ -99,7 +99,7 @@ "Spara app-par" "Tryck på en annan app för att använda delad skärm" "Välj en annan app för att använda delad skärm" - "Avbryt" + "Avbryt" "Avsluta val av delad skärm" "Välj en annan app för att använda delad skärm" "Appen eller organisationen tillåter inte den här åtgärden" @@ -130,18 +130,37 @@ "Snabbinställn." "Aktivitetsfält" "Aktivitetsfältet visas" - "Aktivitetsfältet är dolt" + "Vänster fält och bubblor visas" + "Höger fält och bubblor visas" "Navigeringsfält" "Visa alltid aktivitetsfältet" "Ändra navigeringsläge" "Avdelare för aktivitetsfältet" + "Fler alternativ för aktivitetsfältet" "Flytta högst upp/till vänster" "Flytta längst ned/till höger" - "{count,plural, =1{Visa # app till.}other{Visa # appar till.}}" - "{count,plural, =1{Visa # datorapp.}other{Visa # datorappar.}}" + "Öppna appen som en bubbla" + "Senaste apparna" + "Lista över senaste appar" + "{count,plural, =1{app till}other{appar till}}" + "Skrivbordsläge" "%1$s och %2$s" + "%1$s, objekt %2$d av %3$d" + "Scrolla åt vänster" + "Scrolla åt höger" "Bubbla" "Fler alternativ" "%1$s från %2$s" "%1$s och %2$d till" + "Flytta åt vänster" + "Flytta åt höger" + "Stäng alla" + "utöka %1$s" + "komprimera %1$s" + "Circle to Search" + "Appikon" + "Apptitel" + "Knappen Stäng" + "Fäst i aktivitetsfält" + "Lossa från aktivitetsfält" diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml index 3d8277b44..046e37a01 100644 --- a/quickstep/res/values-sw/strings.xml +++ b/quickstep/res/values-sw/strings.xml @@ -22,11 +22,13 @@ "Bandika" "Muundo huru" "Kompyuta ya mezani" + "Hamishia programu kwenye skrini ya nje" + "Funga" + "Kompyuta ya Mezani" "Hakuna vipengee vya hivi karibuni" "Mipangilio ya matumizi ya programu" "Ondoa zote" "Programu za hivi karibuni" - "Jukumu Limefungwa" "%1$s, %2$s" "< dak 1" "Umebakisha %1$s leo" @@ -45,28 +47,27 @@ "Mapendekezo ya programu yamewashwa" "Umezima mapendekezo ya programu" "Programu iliyotabiriwa: %1$s" + "Mafunzo ya Usogezaji kwa Kutumia Miguso" "Zungusha kifaa chako" - "Tafadhali zungusha kifaa chako ili ukamilishe mafunzo ya usogezaji kwa kutumia ishara" + "Tafadhali zungusha kifaa chako ili ukamilishe mafunzo ya usogezaji kwa kutumia miguso" "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto kabisa" "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto hadi katikati ya skrini na uachilie" "Umejifunza jinsi ya kutelezesha kidole kuanzia kulia ili kurudi nyuma. Sasa jifunze jinsi ya kubadilisha programu." - "Umekamilisha ishara ya kurudi nyuma. Hatua inayofuata, jifunze jinsi ya kubadilisha programu." - "Umeweka ishara ya kurudi nyuma" + "Umekamilisha mafunzo ya miguso ya kurudi nyuma. Hatua inayofuata, fahamu jinsi ya kubadilisha programu." + "Umekamilisha mafunzo ya miguso ya kurudi nyuma" "Hakikisha hutelezeshi kidole karibu sana na sehemu ya chini ya skrini" "Kubadilisha hisi ya ishara ya nyuma, nenda kwenye Mipangilio" "Telezesha kidole ili urudi nyuma" "Ili urudi kwenye skrini iliyotangulia, telezesha kidole kuanzia ukingo wa kushoto au wa kulia kuelekea katikati ya skrini." - "Ili urudi kwenye skrini iliyopita, telezesha vidole viwili kuanzia ukingo wa kushoto au wa kulia kuelekea katikati ya skrini." "Rudi nyuma" "Telezesha kidole kutoka ukingo wa kushoto au kulia hadi katikati ya skrini" "Hakikisha unatelezesha kidole juu kuanzia ukingo wa chini wa skrini" "Hakikisha husitishi kabla ya kuachilia" "Hakikisha unatelezesha kidole juu" - "Umeweka ishara ya kwenda kwenye Skrini ya kwanza. Inayofuata, jifunze jinsi ya kurudi nyuma." - "Umeweka ishara ya kwenda kwenye skrini ya kwanza" + "Umekamilisha mguso wa kwenda kwenye skrini ya kwanza. Inayofuata, fahamu jinsi ya kurudi nyuma." + "Umekamilisha mguso wa kwenda kwenye skrini ya kwanza" "Telezesha kidole ili uende kwenye skrini ya kwanza" "Telezesha kidole juu kuanzia chini ya skrini yako. Ishara hii kila wakati hukupeleka kwenye Skrini ya kwanza." - "Telezesha vidole viwili kuelekea juu kuanzia sehemu ya chini ya skrini. Ishara hii kila wakati hukupeleka kwenye Skrini ya kwanza." "Nenda kwenye ukurasa wa mwanzo" "Telezesha kidole juu kutoka sehemu ya chini ya skrini yako" "Kazi nzuri!" @@ -74,10 +75,9 @@ "Jaribu kushikilia dirisha kwa muda mrefu kabla ya kuachilia" "Hakikisha unatelezesha kidole juu, kisha usitishe" "Umejifunza jinsi ya kutumia ishara. Ili uzime ishara, nenda kwenye Mipangilio." - "Umeweka ishara ya kubadilisha programu" + "Umekamilisha mguso wa kubadilisha programu" "Telezesha kidole ili ubadilishe programu" "Ili ubadili kati ya programu, telezesha kidole juu kuanzia sehemu ya chini ya skrini yako, ushikilie, kisha uachilie." - "Ili ubadilishe kati ya programu, telezesha vidole viwili kuelekea juu kuanzia sehemu ya chini ya skrini yako, ushikilie, kisha uachilie." "Badilisha programu" "Telezesha kidole juu kutoka sehemu ya chini ya skrini yako, shikilia kisha uachilie" "Hongera!" @@ -99,7 +99,7 @@ "Hifadhi jozi ya programu" "Gusa programu nyingine ili utumie kipengele cha kugawa skrini" "Chagua programu nyingine ili utumie hali ya kugawa skrini" - "Ghairi" + "Acha" "Ondoka kwenye hali ya skrini iliyogawanywa" "Chagua programu nyingine ili utumie hali ya kugawa skrini" "Kitendo hiki hakiruhusiwi na programu au shirika lako" @@ -130,18 +130,37 @@ "Mipangilio ya Haraka" "Upauzana" "Upauzana umeonyeshwa" - "Upauzana umefichwa" + "Upauzana na viputo vinaonyeshwa kushoto" + "Upauzana na viputo vinaonyeshwa kulia" "Sehemu ya viungo muhimu" "Onyesha Zana kila wakati" "Badilisha hali ya usogezaji" "Kitenganishi cha Upauzana" + "Upauzana wa Vipengele vya Ziada" "Sogeza juu/kushoto" "Sogeza chini/kulia" - "{count,plural, =1{Onyesha programu # zaidi.}other{Onyesha programu # zaidi.}}" - "{count,plural, =1{Onyesha programu # ya kompyuta ya mezani.}other{Onyesha programu # za kompyuta ya mezani.}}" + "Fungua programu kama kiputo" + "Programu ulizofungua hivi majuzi" + "Orodha ya programu ulizofungua hivi majuzi" + "{count,plural, =1{programu nyingine}other{programu zingine}}" + "Kompyuta ya Mezani" "%1$s na %2$s" + "%1$s, kipengee cha %2$d kati ya %3$d" + "Sogeza kushoto" + "Sogeza kulia" "Kiputo" "Kiputo cha vipengee vya ziada" "%1$s kutoka %2$s" "%1$s na vingine %2$d" + "Sogeza kushoto" + "Sogeza kulia" + "Ondoa vyote" + "panua %1$s" + "kunja %1$s" + "Chora Mviringo ili Kutafuta" + "Aikoni ya programu" + "Kichwa cha programu" + "Kitufe cha kufunga" + "Bandika kwa upauzana" + "Bandua kwa upauzana" diff --git a/quickstep/res/values-sw600dp-land/dimens.xml b/quickstep/res/values-sw600dp-land/dimens.xml index 5e9a177b8..cf7ba0094 100644 --- a/quickstep/res/values-sw600dp-land/dimens.xml +++ b/quickstep/res/values-sw600dp-land/dimens.xml @@ -16,7 +16,7 @@ --> - 48dp + 48dp 48dp @@ -27,10 +27,6 @@ 40dp 49dp - - - 24dp - - 40dp + 24dp diff --git a/quickstep/res/values-sw600dp/dimens.xml b/quickstep/res/values-sw600dp/dimens.xml index e24d8fea7..3e726519a 100644 --- a/quickstep/res/values-sw600dp/dimens.xml +++ b/quickstep/res/values-sw600dp/dimens.xml @@ -33,20 +33,11 @@ 36dp 64dp - - 80dp - - 80dp 24dp - 120dp + 120dp 38sp 15sp - - - - 300dp - diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml index 47d8055e1..9074f6c9c 100644 --- a/quickstep/res/values-ta/strings.xml +++ b/quickstep/res/values-ta/strings.xml @@ -22,11 +22,13 @@ "பின் செய்தல்" "குறிப்பிட்ட வடிவமில்லாத பயன்முறை" "டெஸ்க்டாப்" + "வெளிப்புற டிஸ்ப்ளேவிற்கு நகர்த்துதல்" + "மூடு" + "டெஸ்க்டாப்" "சமீபத்தியவை எதுவுமில்லை" "ஆப்ஸ் உபயோக அமைப்புகள்" "எல்லாம் அழி" "சமீபத்திய ஆப்ஸ்" - "பணி முடிந்தது" "%1$s, %2$s" "< 1 நி" "இன்று %1$s மீதமுள்ளது" @@ -45,6 +47,7 @@ "ஆப்ஸ் பரிந்துரைகள் இயக்கப்பட்டுள்ளன" "ஆப்ஸ் பரிந்துரைகள் முடக்கப்பட்டுள்ளன" "கணித்த ஆப்ஸ்: %1$s" + "சைகை வழிசெலுத்தலுக்கான பயிற்சி" "உங்கள் சாதனத்தைச் சுழற்றுங்கள்" "சைகை வழிசெலுத்தல் பயிற்சியை நிறைவுசெய்ய உங்கள் சாதனத்தைச் சுழற்றுங்கள்" "வலது அல்லது இடது ஓரத்தின் விளிம்பிலிருந்து ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" @@ -56,7 +59,6 @@ "பின்செல் சைகையின் உணர்திறனை மாற்ற அமைப்புகளுக்குச் செல்க" "பின்செல்ல ஸ்வைப் செய்யுங்கள்" "முந்தைய திரைக்கு மீண்டும் செல்ல, இடது/வலது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்க." - "முந்தைய திரைக்கு மீண்டும் செல்ல, 2 விரல்களால் இடது அல்லது வலது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்யுங்கள்." "பின்செல்லுதல்" "வலது அல்லது இடது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்யுங்கள்" "திரையின் கீழ் ஓரத்திலிருந்து மேல்நோக்கி ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" @@ -66,18 +68,16 @@ "முகப்புக்குச் செல் சைகைப் பயிற்சியை நிறைவுசெய்துவிட்டீர்கள்" "முகப்புக்குச் செல்ல ஸ்வைப் செய்யுங்கள்" "திரையின் கீழிருந்து மேலாக ஸ்வைப் செய்க. இந்தச் சைகை எப்போதும் முகப்புத் திரைக்கு அழைத்துச் செல்லும்." - "2 விரலால் திரையின் கீழிருந்து மேலாக ஸ்வைப் செய்க. இந்தச் சைகை முகப்புத் திரைக்கு அழைத்துச் செல்லும்." "முகப்புக்குச் செல்லுதல்" "திரையின் கீழிருந்து மேல்நோக்கி ஸ்வைப் செய்யுங்கள்" "அருமை!" "திரையின் கீழ் ஓரத்திலிருந்து மேல்நோக்கி ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" "விடுவிப்பதற்கு முன்பாக நீண்டநேரம் சாளரத்தை அழுத்திப் பிடித்திருங்கள்" - "மேல்நோக்கி நேராக ஸ்வைப் செய்தபிறகு இடைநிறுத்துவதை உறுதிசெய்துகொள்ளுங்கள்" + "மேல்நோக்கி நேராக ஸ்வைப் செய்தபிறகு சற்றுநேரம் அழுத்திபடியே வைத்திருங்கள்" "சைகைகளை எப்படி உபயோகிப்பது என்று கற்றுக்கொண்டீர்கள். சைகைகளை முடக்க அமைப்புகளுக்குச் செல்லுங்கள்." "ஆப்ஸுக்கிடையே மாறும் சைகைப் பயிற்சியை நிறைவுசெய்துவிட்டீர்கள்" "ஆப்ஸுக்கிடையே மாற ஸ்வைப் செய்யுங்கள்" "ஆப்ஸுக்கு இடையே மாற, திரையின் கீழிலிருந்து மேலாக ஸ்வைப் செய்து, பிடித்திருந்து, பிறகு விடுவிக்கவும்." - "ஆப்ஸுக்கிடையே மாற, திரையின் கீழிருந்து மேலாக 2 விரலால் ஸ்வைப் செய்து, பிடித்து, பிறகு விடுவிக்கவும்." "ஆப்ஸுக்கிடையே மாறுதல்" "உங்கள் திரையின் கீழ்ப்பகுதியில் இருந்து மேலே ஸ்வைப் செய்து, பிடித்து, பிறகு விடுவியுங்கள்" "அருமை!" @@ -90,7 +90,7 @@ "அனைத்தையும் அமைத்துவிட்டீர்கள்!" "முகப்புக்குச் செல்ல மேல்நோக்கி ஸ்வைப் செய்யுங்கள்" "முகப்புத் திரைக்குச் செல்வதற்கு முகப்பு பட்டனைத் தட்டவும்" - "உங்கள் %1$s சாதனத்தைப் பயன்படுத்தத் தயாராகிவிட்டீர்கள்" + "உங்கள் %1$s உங்களுக்காகத் தயாராக இருக்கிறது" "சாதனம்" "சிஸ்டம் வழிசெலுத்தல் அமைப்புகள்" "பகிர்" @@ -99,7 +99,7 @@ "ஆப்ஸ் ஜோடியைச் சேமி" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தேர்வுசெய்யுங்கள்" - "ரத்துசெய்" + "ரத்துசெய்" "திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்" "திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க" "ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை" @@ -130,18 +130,37 @@ "விரைவு அமைப்புகள்" "செயல் பட்டி" "செயல் பட்டி காட்டப்படுகிறது" - "செயல் பட்டி மறைக்கப்பட்டுள்ளது" + "செயல் பட்டி & குமிழை இடதுபுறம் காட்டும்" + "செயல் பட்டி & குமிழை வலதுபுறம் காட்டும்" "வழிசெலுத்தல் பட்டி" "செயல் பட்டியை எப்போதும் காட்டு" "வழிசெலுத்தல் பயன்முறையை மாற்று" "செயல் பட்டிப் பிரிப்பான்" + "செயல் பட்டிக்கான கூடுதல் விருப்பங்கள்" "மேலே/இடதுபுறம் நகர்த்தும்" "கீழே/வலதுபுறம் நகர்த்தும்" - "{count,plural, =1{மேலும் # ஆப்ஸைக் காட்டு.}other{மேலும் # ஆப்ஸைக் காட்டு.}}" - "{count,plural, =1{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}other{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}}" + "ஆப்ஸைக் குமிழாகத் திற" + "சமீபத்திய ஆப்ஸ்" + "சமீபத்திய ஆப்ஸ் பட்டியல்" + "{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}" + "டெஸ்க்டாப்" "%1$s மற்றும் %2$s" + "%1$s, %3$d இல் %2$d கட்டம்" + "இடதுபுறம் நகர்த்தும்" + "வலதுபுறம் நகர்த்தும்" "குமிழ்" "கூடுதல் விருப்பங்களைக் காட்டும்" "%2$s வழங்கும் %1$s" "%1$s மற்றும் %2$d" + "இடதுபுறம் நகர்த்தும்" + "வலதுபுறம் நகர்த்தும்" + "அனைத்தையும் மூடும்" + "%1$s ஐ விரிவாக்கும்" + "%1$s ஐச் சுருக்கும்" + "வட்டமிட்டுத் தேடல்" + "ஆப்ஸ் ஐகான்" + "ஆப்ஸ் தலைப்பு" + "மூடுவதற்கான பட்டன்" + "செயல்பட்டியில் பின் செய்" + "செயல்பட்டியிலிருந்து அகற்று" diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml index a4e1cbf09..28852236c 100644 --- a/quickstep/res/values-te/strings.xml +++ b/quickstep/res/values-te/strings.xml @@ -22,11 +22,13 @@ "పిన్ చేయండి" "సంప్రదాయేతర" "డెస్క్‌టాప్" + "ఎక్స్‌టర్నల్ డిస్‌ప్లేకు తరలించండి" + "మూసివేయండి" + "డెస్క్‌టాప్" "ఇటీవలి ఐటెమ్‌లు ఏవీ లేవు" "యాప్ వినియోగ సెట్టింగ్‌లు" "అన్నీ తీసివేయండి" "ఇటీవలి యాప్‌లు" - "టాస్క్ మూసివేయబడింది" "%1$s, %2$s" "< 1 నిమిషం" "నేటికి %1$s మిగిలి ఉంది" @@ -45,6 +47,7 @@ "యాప్ సలహాలు ఎనేబుల్ చేయబడ్డాయి" "యాప్ సూచ‌న‌లు డిజేబుల్‌ చేయబడ్డాయి" "సూచించబడిన యాప్: %1$s" + "సంజ్ఞ నావిగేషన్ ట్యుటోరియల్" "మీ పరికరాన్ని రొటేట్ చేయండి" "సంజ్ఞ నావిగేషన్ ట్యుటోరియల్‌ను పూర్తి చేయడానికి దయచేసి మీ పరికరాన్ని రొటేట్ చేయండి" "కుడి వైపు చిట్ట చివరి లేదా ఎడమ వైపు చిట్ట చివరి అంచు నుండి స్వైప్ చేస్తున్నారని నిర్ధారించుకోండి" @@ -56,7 +59,6 @@ "వెనుక సంజ్ఞ సున్నితత్వం మార్చడానికి, సెట్టింగ్‌లకు వెళ్లండి" "వెనుకకు వెళ్ళడం కోసం స్వైప్ చేయండి" "మునుపటి స్క్రీన్‌కు తిరిగి వెళ్లడానికి, ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యలోకి స్వయిప్ చేయండి." - "గత స్క్రీన్‌కు తిరిగి వెళ్లడానికి, ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యలోకి 2 వేళ్లతో స్వైప్ చేయండి." "వెనుకకు వెళ్లండి" "ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యకు స్వైప్ చేయండి" "మీరు స్క్రీన్ దిగువ అంచు నుండి పైకి స్వైప్ చేశారని నిర్ధారించుకోండి" @@ -66,7 +68,6 @@ "మీరు మొదటి స్క్రీన్‌కు వెళ్లే సంజ్ఞను పూర్తి చేశారు" "మొదటి స్క్రీన్‌కు వెళ్లడానికి స్వైప్ చేయండి" "స్క్రీన్ కింది నుండి పైకి స్వైప్ చేయండి. ఈ సంజ్ఞ ఎప్పుడూ మిమ్మల్ని మొదటి స్క్రీన్‌కు తీసుకెళ్తుంది." - "స్క్రీన్ కింది నుండి 2 వేళ్లతో పైకి స్వైప్ చేయండి. సంజ్ఞ ఎల్లప్పుడూ మొదటి స్క్రీన్‌కు తీసుకెళ్తుంది." "మొదటి ట్యాబ్‌కు వెళ్లండి" "స్క్రీన్ కింది భాగం నుండి పైకి స్వైప్ చేయండి" "బాగా చేశారు!" @@ -74,10 +75,9 @@ "వేలిని రిలీజ్ చేయడానికి ముందు విండోను ఎక్కువసేపు నొక్కి, పట్టుకోవడానికి ట్రై చేయండి" "స్క్రీన్‌పై నేరుగా పైకి స్వైప్ చేసి, ఆపై పాజ్ చేయండి" "మీరు సంజ్ఞలను ఎలా ఉపయోగించాలో నేర్చుకున్నారు. సంజ్ఞలను ఆఫ్ చేయడానికి, సెట్టింగ్‌లకు వెళ్లండి." - "మీరు \'యాప్‌ల మధ్య మార్పు\' సంజ్ఞను పూర్తి చేశారు" + "మీరు \'యాప్‌ల మధ్య మారేందుకు సంజ్ఞ\' ట్యుటోరియల్‌ను పూర్తి చేశారు" "యాప్‌ల మధ్య మార్చడం కోసం స్వైప్ చేయండి" "యాప్‌ల మధ్య మారడానికి, మీ స్క్రీన్ కింది వైపు నుండి పైకి స్వైప్ చేసి, పట్టుకుని, తర్వాత వదలండి." - "యాప్‌ల మధ్య మారడానికి, మీ స్క్రీన్ కింది నుండి 2 వేళ్లతో పైకి స్వైప్ చేసి, నొక్కి పట్టి, వదలండి." "యాప్‌ల మధ్య స్విచ్ అవ్వండి" "మీ స్క్రీన్ కింది వైపు నుండి పైకి స్వైప్ చేసి, పట్టుకుని, తర్వాత వదలండి" "చాలా బాగా చేశారు!" @@ -99,7 +99,7 @@ "యాప్ పెయిర్‌ను సేవ్ చేయండి" "స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి" "స్ప్లిట్ స్క్రీన్‌ను ఉపయోగించడానికి మరొక యాప్ ఎంచుకోండి" - "రద్దు చేయండి" + "రద్దు చేయండి" "స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి" "స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి" "ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు" @@ -130,18 +130,37 @@ "క్విక్ సెట్టింగ్‌లు" "టాస్క్‌బార్" "టాస్క్‌బార్ చూపబడింది" - "టాస్క్‌బార్ దాచబడింది" + "టాస్క్‌బార్, బబుల్స్ ఎడమవైపున చూపబడ్డాయి" + "టాస్క్‌బార్, బబుల్స్ కుడివైపున చూపబడ్డాయి" "నావిగేషన్ బార్" "టాస్క్‌బార్‌ను నిరంతరం చూపండి" "నావిగేషన్ మోడ్‌ను మార్చండి" "టాస్క్‌బార్ డివైడర్" + "టాస్క్‌బార్ ఓవర్‌ఫ్లో" "ఎగువ/ఎడమ వైపునకు తరలించండి" "దిగువ/కుడి వైపునకు తరలించండి" - "{count,plural, =1{మరో # యాప్‌ను చూడండి.}other{మరో # యాప్‌లను చూడండి.}}" - "{count,plural, =1{# డెస్క్‌టాప్ యాప్‌ను చూపండి.}other{# డెస్క్‌టాప్ యాప్‌లను చూపండి.}}" + "యాప్‌ను బబుల్‌లాగా తెరవండి" + "ఇటీవలి యాప్‌లు" + "ఇటీవలి యాప్ లిస్ట్" + "{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}" + "డెస్క్‌టాప్" "%1$s, %2$s" + "%1$s, %3$d‌లో %2$d‌వ ఐటెమ్" + "ఎడమవైపునకు స్క్రోల్ చేయండి" + "కుడివైపునకు స్క్రోల్ చేయండి" "బబుల్" "ఓవర్‌ఫ్లో" "%2$s నుండి %1$s" "%1$s, మరో %2$d" + "ఎడమ వైపుగా జరపండి" + "కుడి వైపుగా జరపండి" + "అన్నింటినీ విస్మరించండి" + "%1$sను విస్తరించండి" + "%1$sను కుదించండి" + "సెర్చ్ చేయడానికి సర్కిల్ గీయండి" + "యాప్ చిహ్నం" + "యాప్ టైటిల్" + "\'మూసివేయండి\' బటన్" + "టాస్క్‌బార్‌కు పిన్" + "టాస్క్‌బార్ అన్‌పిన్" diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml index 1bbb137a1..09d0df14a 100644 --- a/quickstep/res/values-th/strings.xml +++ b/quickstep/res/values-th/strings.xml @@ -22,11 +22,13 @@ "ปักหมุด" "รูปแบบอิสระ" "เดสก์ท็อป" + "ย้ายไปยังจอแสดงผลภายนอก" + "ปิด" + "เดสก์ท็อป" "ไม่มีรายการล่าสุด" "การตั้งค่าการใช้แอป" "ล้างทั้งหมด" "แอปล่าสุด" - "ปิดงานแล้ว" "%1$s %2$s" "<1 นาที" "วันนี้เหลืออีก %1$s" @@ -45,39 +47,37 @@ "เปิดใช้แอปแนะนำแล้ว" "ปิดใช้คำแนะนำเกี่ยวกับแอปอยู่" "แอปที่คาดว่าจะใช้: %1$s" + "บทแนะนำการไปยังส่วนต่างๆ ด้วยท่าทางสัมผัส" "หมุนอุปกรณ์ของคุณ" - "โปรดหมุนอุปกรณ์เพื่อทำตามบทแนะนำการนำทางด้วยท่าทางสัมผัสให้เสร็จสมบูรณ์" - "ตรวจสอบว่าปัดจากขอบด้านขวาสุดหรือซ้ายสุด" + "โปรดหมุนอุปกรณ์เพื่อทำตามบทแนะนำการไปยังส่วนต่างๆ ด้วยท่าทางสัมผัสให้เสร็จสมบูรณ์" + "ปัดจากขอบด้านขวาสุดหรือซ้ายสุด" "ตรวจสอบว่าปัดจากขอบด้านขวาหรือซ้ายไปตรงกลางหน้าจอ แล้วยกนิ้วขึ้น" "คุณรู้วิธีปัดจากด้านขวาเพื่อย้อนกลับแล้ว ต่อไปดูวิธีสลับแอป" - "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว ต่อไปดูวิธีสลับแอป" - "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว ต่อไปดูวิธีสลับแอป" + "คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว" "ไม่ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป" "เปลี่ยนความไวของท่าทางสัมผัสเพื่อย้อนกลับได้ที่การตั้งค่า" "ปัดเพื่อย้อนกลับ" "หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" - "หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ใช้ 2 นิ้วปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" "ย้อนกลับ" "ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" "ปัดขึ้นจากขอบด้านล่างของหน้าจอ" "ไม่ต้องหยุดชั่วคราวก่อนยกนิ้วขึ้น" "ปัดขึ้นในแนวตรง" - "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว ต่อไปดูวิธีย้อนกลับ" - "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าจอหลักสำเร็จแล้ว ต่อไปดูวิธีย้อนกลับ" + "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าจอหลักสำเร็จแล้ว" "ปัดเพื่อไปที่หน้าแรก" "ปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ" - "ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ" "ไปที่หน้าจอหลัก" "ปัดขึ้นจากด้านล่างของหน้าจอ" "เก่งมาก" "ปัดขึ้นจากขอบด้านล่างของหน้าจอ" "ลองแตะหน้าต่างค้างไว้นานขึ้นก่อนปล่อยนิ้ว" - "ตรวจสอบว่าปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว" + "ปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว" "คุณรู้วิธีใช้ท่าทางสัมผัสแล้ว หากต้องการปิดท่าทางสัมผัส ให้ไปที่การตั้งค่า" - "คุณทำท่าทางสัมผัสเพื่อสลับแอปเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อเปลี่ยนแอปสำเร็จแล้ว" "ปัดเพื่อสลับแอป" "หากต้องการสลับระหว่างแอปต่างๆ ให้ปัดขึ้นจากด้านล่างของหน้าจอ ค้างไว้ แล้วปล่อย" - "หากต้องการสลับระหว่างแอป ให้ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอค้างไว้แล้วปล่อย" "เปลี่ยนแอป" "ปัดขึ้นจากด้านล่างของหน้าจอ ค้างไว้ แล้วปล่อย" "เยี่ยมมาก" @@ -99,7 +99,7 @@ "บันทึกคู่แอป" "แตะแอปอื่นเพื่อใช้การแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" - "ยกเลิก" + "ยกเลิก" "ออกจากการเลือกโหมดแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" "แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้" @@ -130,18 +130,37 @@ "การตั้งค่าด่วน" "แถบงาน" "แถบงานแสดงอยู่" - "แถบงานซ่อนอยู่" + "แถบงานและบับเบิลแสดงไว้ทางซ้าย" + "แถบงานและบับเบิลแสดงไว้ทางขวา" "แถบนำทาง" "แสดงแถบงานเสมอ" "เปลี่ยนโหมดการนําทาง" "ตัวแบ่งแถบงาน" + "การดำเนินการเพิ่มเติมของแถบงาน" "ย้ายไปที่ด้านบนหรือด้านซ้าย" "ย้ายไปที่ด้านล่างหรือด้านขวา" - "{count,plural, =1{แสดงเพิ่มเติมอีก # แอป}other{แสดงเพิ่มเติมอีก # แอป}}" - "{count,plural, =1{แสดงแอปบนเดสก์ท็อป # รายการ}other{แสดงแอปบนเดสก์ท็อป # รายการ}}" + "เปิดแอปเป็นบับเบิล" + "แอปล่าสุด" + "รายการแอปล่าสุด" + "{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}" + "เดสก์ท็อป" "%1$s และ %2$s" + "%1$s, รายการที่ %2$d จาก %3$d" + "เลื่อนไปทางซ้าย" + "เลื่อนไปทางขวา" "บับเบิล" "การดำเนินการเพิ่มเติม" "%1$s จาก %2$s" "%1$s และอีก %2$d รายการ" + "ย้ายไปทางซ้าย" + "ย้ายไปทางขวา" + "ปิดทั้งหมด" + "ขยาย %1$s" + "ยุบ %1$s" + "วงเพื่อค้นหา" + "ไอคอนแอป" + "ชื่อแอป" + "ปุ่มปิด" + "ปักหมุดไปยังแถบงาน" + "เลิกปักหมุดจากแถบงาน" diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml index 978a5a37f..35515789c 100644 --- a/quickstep/res/values-tl/strings.xml +++ b/quickstep/res/values-tl/strings.xml @@ -22,11 +22,13 @@ "I-pin" "Freeform" "Desktop" + "Ilipat sa external na display" + "Isara" + "Desktop" "Walang kamakailang item" "Mga setting ng paggamit ng app" "I-clear lahat" "Mga kamakailang app" - "Isinara ang Gawain" "%1$s, %2$s" "< 1 min" "%1$s na lang ngayon" @@ -45,6 +47,7 @@ "Naka-enable ang mga iminumungkahing app" "Naka-disable ang mga iminumungkahing app" "Hinulaang app: %1$s" + "Tutorial sa Navigation gamit ang Galaw" "I-rotate ang iyong device" "Paki-rotate ang iyong device para tapusin ang tutorial sa navigation gamit ang galaw" "Tiyaking magsa-swipe ka mula sa dulong kanan o dulong kaliwang gilid" @@ -56,7 +59,6 @@ "Pumunta sa Settings para baguhin ang sensitivity ng pagbalik" "Mag-swipe para bumalik" "Para bumalik sa nakaraang screen, mag-swipe mula sa kaliwa o kanang gilid patungo sa gitna ng screen." - "Para bumalik sa huling screen, mag-swipe gamit ang 2 daliri mula sa kaliwa o kanang gilid hanggang sa gitna ng screen." "Bumalik" "Mag-swipe mula sa kaliwa o kanang gilid papunta sa gitna ng screen" "Tiyaking magsa-swipe ka pataas mula sa pinakaibaba ng screen" @@ -66,7 +68,6 @@ "Nakumpleto mo na ang galaw para pumunta sa home" "Mag-swipe para pumunta sa home" "Mag-swipe pataas mula sa ibaba ng iyong screen. Dadalhin ka palagi ng galaw na ito sa Home screen." - "Mag-swipe pataas gamit ang 2 daliri mula sa ibaba ng screen. Dadalhin ka palagi nito sa Home screen." "Pumunta sa home" "Mag-swipe pataas mula sa ibabang bahagi ng iyong screen" "Magaling!" @@ -77,7 +78,6 @@ "Nakumpleto mo na ang galaw para magpalipat-lipat sa mga app" "Mag-swipe para lumipat ng app" "Para lumipat ng app, mag-swipe pataas mula sa ibaba ng iyong screen, mag-hold, at iangat ang daliri." - "Para lumipat ng app, mag-swipe pataas gamit ang 2 daliri mula sa ibaba, mag-hold, at bumitaw." "Lumipat ng app" "Mag-swipe pataas mula sa ibaba ng iyong screen, i-hold ito saka bitawan" "Magaling!" @@ -99,7 +99,7 @@ "I-save ang app pair" "Mag-tap ng ibang app para gamitin ang split screen" "Pumili ng ibang app para gamitin ang split screen" - "Kanselahin" + "Kanselahin" "Lumabas sa pagpili ng split screen" "Pumili ng ibang app para gamitin ang split screen" "Hindi pinapayagan ng app o ng iyong organisasyon ang pagkilos na ito" @@ -130,18 +130,37 @@ "Quick Settings" "Taskbar" "Ipinapakita ang taskbar" - "Nakatago ang taskbar" + "Taskbar at bubble sa kaliwa" + "Taskbar at bubble sa kanan" "Navigation bar" "Ipakita lagi ang Taskbar" "Magpalit ng navigation mode" "Divider ng Taskbar" + "Taskbar Overflow" "Ilipat sa itaas/kaliwa" "Ilipat sa ibaba/kanan" - "{count,plural, =1{Magpakita ng # pang app.}one{Magpakita ng # pang app.}other{Magpakita ng # pang app.}}" - "{count,plural, =1{Ipakita ang # desktop app.}one{Ipakita ang # desktop app.}other{Ipakita ang # na desktop app.}}" + "Buksan ang app bilang bubble" + "Mga kamakailang app" + "Kamakailang listahan ng app" + "{count,plural, =1{pang app}one{pang app}other{pang app}}" + "Desktop" "%1$s at %2$s" + "%1$s, item %2$d ng %3$d" + "Mag-scroll pakaliwa" + "Mag-scroll pakanan" "Bubble" "Overflow" "%1$s mula sa %2$s" "%1$s at %2$d pa" + "Ilipat pakaliwa" + "Ilipat pakanan" + "I-dismiss lahat" + "i-expand ang %1$s" + "i-collapse ang %1$s" + "Circle to Search" + "Icon ng app" + "Pamagat ng app" + "Button na isara" + "I-pin sa taskbar" + "I-unpin sa taskbar" diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml index 0cc5d7f85..5c5fba656 100644 --- a/quickstep/res/values-tr/strings.xml +++ b/quickstep/res/values-tr/strings.xml @@ -22,11 +22,13 @@ "Sabitle" "Serbest çalışma" "Masaüstü" + "Harici ekrana taşı" + "Kapat" + "Masaüstü" "Yeni öğe yok" "Uygulama kullanım ayarları" "Tümünü temizle" "Son uygulamalar" - "Görev Kapatıldı" "%1$s, %2$s" "< 1 dk." "Bugün %1$s kaldı" @@ -45,6 +47,7 @@ "Uygulama önerileri etkinleştirildi" "Uygulama önerileri devre dışı bırakıldı" "Tahmin edilen uygulama: %1$s" + "Hareketle Gezinme Eğitimi" "Cihazınızı döndürün" "Hareketle gezinme eğitimini tamamlamak için lütfen cihazınızı döndürün" "En sağ veya en sol kenardan kaydırdığınızdan emin olun" @@ -56,7 +59,6 @@ "Geri hareketinin hassasiyetini değiştirmek için Ayarlar\'a gidin" "Geri dönmek için kaydırma" "Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru kaydırın." - "Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru 2 parmağınızla kaydırın." "Geri dönme" "Sol veya sağ kenardan ekranın ortasına doğru kaydırın" "Ekranın alt kenarından yukarı kaydırdığınızdan emin olun" @@ -66,7 +68,6 @@ "Ana ekrana git hareketini tamamladınız" "Ana ekrana gitmek için kaydırma" "Ekranın alt kısmından yukarıya doğru kaydırın. Bu hareket sizi her zaman Ana ekrana götürür." - "Ekranın alt kısmından 2 parmağınızla yukarı kaydırın. Bu hareket sizi her zaman Ana ekrana götürür." "Ana sayfaya gitme" "Parmağınızı ekranın alt kısmından yukarıya doğru kaydırın" "Tebrikler!" @@ -77,7 +78,6 @@ "Uygulamalar arasında geçiş yapma hareketini tamamladınız" "Uygulamalar arasında geçiş yapmak için kaydırma" "Uygulamalar arasında geçiş yapmak için ekranınızın altından yukarı kaydırıp basılı tutun ve sonra bırakın." - "Uygulamalara geçiş yapmak için ekranın altından 2 parmakla yukarı kaydırıp basılı tutun ve bırakın." "Uygulamalar arasında geçiş yapma" "Ekranınızın alt tarafından yukarı doğru kaydırın, tutun ve sonra bırakın" "Tebrikler!" @@ -87,10 +87,10 @@ "Tekrar deneyin" "Güzel!" "Eğitim %1$d/%2$d" - "İşlem tamam!" + "Kurulum tamamlandı" "Ana ekrana gitmek için yukarı kaydırın" "Ana ekranınıza gitmek için ana sayfa düğmesine dokunun" - "%1$s cihazınızı kullanmaya hazırsınız" + "Artık %1$s kullanılmak için hazır" "cihaz" "Sistem gezinme ayarları" "Paylaş" @@ -99,7 +99,7 @@ "Uygulama çiftini kaydet" "Bölünmüş ekran için başka bir uygulamaya dokunun" "Bölünmüş ekran kullanmak için başka bir uygulama seçin" - "İptal" + "İptal" "Bölünmüş ekran seçiminden çıkın" "Bölünmüş ekran kullanmak için başka bir uygulama seçin" "Uygulamanız veya kuruluşunuz bu işleme izin vermiyor" @@ -130,18 +130,37 @@ "Hızlı Ayarlar" "Görev çubuğu." "Görev çubuğu gösteriliyor" - "Görev çubuğu gizlendi" + "Görev çubuğu ve baloncuklar solda gösteriliyor" + "Görev çubuğu ve baloncuklar sağda gösteriliyor" "Gezinme çubuğu" "Görev çubuğunu daima göster" "Gezinme modunu değiştir" "Görev Çubuğu Ayırıcısı" + "Görev Çubuğu Taşması" "Sol üste taşı" "Sağ alta taşı" - "{count,plural, =1{# uygulama daha göster.}other{# uygulama daha göster}}" - "{count,plural, =1{# masaüstü uygulamasını göster.}other{# masaüstü uygulamasını göster.}}" + "Uygulamayı balon olarak aç" + "Son uygulamalar" + "Son uygulama listesi" + "{count,plural, =1{uygulama daha}other{uygulama daha}}" + "Masaüstü" "%1$s ve %2$s" + "%1$s, %2$d/%3$d öğe" + "Sola kaydır" + "Sağa kaydır" "Balon" "Taşma" "%2$s uygulamasından %1$s" "%1$s ve %2$d tane daha" + "Sola taşı" + "Sağa taşı" + "Tümünü kapat" + "genişlet: %1$s" + "daralt: %1$s" + "Seçerek Arat" + "Uygulama simgesi" + "Uygulama başlığı" + "Kapat düğmesi" + "Çubuğa sabitle" + "Çubuktan kaldır" diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml index 9c706a840..1d1f040ac 100644 --- a/quickstep/res/values-uk/strings.xml +++ b/quickstep/res/values-uk/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Закріпити" "Довільна форма" - "Комп’ютер" + "Робочий стіл" + "Перемістити на зовнішній екран" + "Закрити" + "Комп’ютер" "Немає нещодавніх додатків" "Налаштування використання додатка" "Очистити все" "Нещодавні додатки" - "Завдання закрито" "%1$s, %2$s" "< 1 хв" "Сьогодні залишилося %1$s" @@ -45,6 +47,7 @@ "Рекомендовані додатки ввімкнено" "Рекомендовані додатки вимкнено" "Передбачений додаток: %1$s" + "Посібник із навігації за допомогою жестів" "Оберніть пристрій" "Обертайте пристрій, щоб ознайомитися з посібником із навігації за допомогою жестів" "Проведіть пальцем від самого краю екрана (правого або лівого)" @@ -56,7 +59,6 @@ "Щоб змінити чутливість жесту \"Назад\", відкрийте налаштування" "Щоб повернутися, проведіть пальцем по екрану" "Щоб перейти на попередній екран, проведіть пальцем від лівого чи правого краю до середини екрана." - "Щоб перейти на попередній екран, проведіть двома пальцями від лівого чи правого краю до середини екрана." "Повернення на попередній екран" "Проведіть пальцем від лівого чи правого краю до середини екрана" "Проведіть пальцем угору від нижнього краю екрана" @@ -66,7 +68,6 @@ "Ви виконали жест переходу на головний екран" "Проведіть пальцем, щоб перейти на головний екран" "Проведіть пальцем по екрану знизу вгору. Цей жест завжди повертатиме вас на головний екран." - "Проведіть двома пальцями вгору від низу екрана. Цей жест завжди спрямовує вас на головний екран." "Перехід на головний екран" "Проведіть пальцем угору від низу екрана" "Чудово!" @@ -77,7 +78,6 @@ "Ви виконали жест переходу в інший додаток" "Проведіть пальцем, щоб перейти в інший додаток" "Щоб переключатися між додатками, проведіть знизу вгору по екрану, утримуйте палець, а потім відпустіть." - "Щоб перейти в інший додаток, проведіть 2 пальцями від низу екрана, потримайте й відпустіть палець." "Перемикання між додатками" "Проведіть пальцем знизу вгору, утримуйте палець на екрані, а потім відпустіть" "Чудово!" @@ -92,14 +92,14 @@ "Натисніть кнопку головного екрана, щоб відкрити його" "Тепер ви можете використовувати %1$s" "пристрій" - "Системні налаштування навігації" + "Налаштування навігації в системі" "Поділитися" "Знімок екрана" "Розділити" "Зберегти пару" "Щоб розділити екран, виберіть ще один додаток." "Щоб розділити екран, виберіть ще один додаток." - "Скасувати" + "Скасувати" "Вийти з режиму розділення екрана" "Щоб розділити екран, виберіть ще один додаток." "Ця дія заборонена додатком або адміністратором організації" @@ -130,18 +130,37 @@ "Швидкі налаштув." "Панель завдань" "Панель завдань показано" - "Панель завдань приховано" + "Панель завдань і чати – зліва" + "Панель завдань і чати – справа" "Панель навігації" "Завжди показув. панель завдань" "Змінити режим навігації" "Розділювач панелі завдань" + "Додаткове меню панелі завдань" "Перемістити вгору або вліво" "Перемістити вниз або вправо" - "{count,plural, =1{Показати ще # додаток.}one{Показати ще # додаток.}few{Показати ще # додатки.}many{Показати ще # додатків.}other{Показати ще # додатка.}}" - "{count,plural, =1{Показати # комп’ютерну програму.}one{Показати # комп’ютерну програму.}few{Показати # комп’ютерні програми.}many{Показати # комп’ютерних програм.}other{Показати # комп’ютерної програми.}}" + "Відкрити додаток у спливаючому вікні" + "Нещодавні додатки" + "Список нещодавніх додатків" + "{count,plural, =1{інший додаток}one{інший додаток}few{інші додатки}many{інших додатків}other{іншого додатка}}" + "Комп’ютер" "%1$s та %2$s" + "%1$s, об’єкт %2$d з %3$d" + "Прокрутити вліво" + "Прокрутити вправо" "Повідомлення" "Додаткове повідомлення" "%1$s з додатка %2$s" "%1$s і ще %2$d" + "Перемістити вліво" + "Перемістити вправо" + "Закрити все" + "розгорнути \"%1$s\"" + "згорнути \"%1$s\"" + "Обвести й знайти" + "Значок додатка" + "Назва додатка" + "Кнопка \"Закрити\"" + "На панель завдань" + "З панелі завдань" diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml index e12524868..8c44ef71a 100644 --- a/quickstep/res/values-ur/strings.xml +++ b/quickstep/res/values-ur/strings.xml @@ -22,11 +22,13 @@ "پن کریں" "فری فارم" "ڈیسک ٹاپ" + "بیرونی ڈسپلے پر متقل کریں" + "بند کریں" + "ڈیسک ٹاپ" "کوئی حالیہ آئٹم نہیں" "ایپ کے استعمال کی ترتیبات" "سبھی کو صاف کریں" "حالیہ ایپس" - "ٹاسک بند ہے" "%1$s،%2$s" "‏< 1 منٹ" "آج %1$s بچا ہے" @@ -45,6 +47,7 @@ "ایپ کی تجاویز فعال ہیں" "ایپ کی تجاویز غیر فعال ہیں" "پیشن گوئی کردہ ایپ: %1$s" + "اشاروں والی نیویگیشن ٹیوٹوریل" "اپنا آلہ گھمائیں" "براہ کرم اشاروں والی نیویگیشن کا ٹیوٹوریل مکمل کرنے کے لیے اپنا آلہ گھمائیں" "یقینی بنائیں کہ آپ دائیں یا بائیں کنارے سے دور سے سوائپ کریں" @@ -56,7 +59,6 @@ "پچھلے اشارے کی حساسیت تبدیل کرنے کے لیے ترتیبات پر جائیں" "واپس جانے کے لیے سوائپ کریں" "پچھلی اسکرین پر واپس جانے کے لیے بائیں یا دائیں کنارے سے اسکرین کے وسط تک سوائپ کریں۔" - "آخری اسکرین پر واپس جانے کے لیے، 2 انگلیوں سے بائیں یا دائیں کنارے سے اسکرین کے وسط تک سوائپ کریں۔" "واپس جائیں" "دائیں یا بائیں کنارے سے اسکرین کے وسط تک سوائپ کریں" "اس بات کو یقینی بنائیں کہ آپ اسکرین کے نچلے کنارے سے اوپر کی طرف سوائپ کریں" @@ -66,7 +68,6 @@ "آپ نے ہوم پر جانے کا اشارہ مکمل کر لیا" "ہوم پر جانے کے لیے سوائپ کریں" "اپنی اسکرین کے نیچے سے اوپر کی طرف سوائپ کریں۔ یہ اشارہ آپ کو ہمیشہ ہوم اسکرین پر لے جاتا ہے۔" - "اسکرین کے نیچے سے 2 انگلیوں سے اوپر سوائپ کریں۔ یہ اشارہ آپ کو ہمیشہ ہوم اسکرین پر لے جاتا ہے۔" "ہوم پر جائیں" "اپنی اسکرین کے نچلے حصے سے اوپر کی طرف سوائپ کریں" "بہترین!" @@ -77,7 +78,6 @@ "آپ نے ایپس کو سوئچ کرنے کا اشارہ مکمل کر لیا" "ایپس سوئچ کرنے کے لیے سوائپ کریں" "ایپس کے مابین سوئچ کرنے کے لیے، اپنی اسکرین کے نچلے حصے سے اوپر کی جانب سوائپ کریں، پکڑے رکھیں، پھر چھوڑ دیں۔" - "ایپس کے مابین سوئچ کرنے کیلئے، اپنی اسکرین کے نیچے سے 2 انگلیوں سے اوپر سوائپ کریں، دبائے رکھیں پھر چھوڑ دیں۔" "ایپس سوئچ کریں" "اپنی اسکرین کے نچلے حصے سے اوپر کی جانب سوائپ کریں، دبائے رکھیں، پھر چھوڑ دیں" "بہت خوب!" @@ -99,7 +99,7 @@ "ایپس کے جوڑے کو محفوظ کریں" "اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" - "منسوخ کریں" + "منسوخ کریں" "اسپلٹ اسکرین کے انتخاب سے باہر نکلیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" "ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے" @@ -130,18 +130,37 @@ "فوری ترتیبات" "ٹاسک بار" "ٹاشک بار دکھایا گیا" - "ٹاسک بار چھپایا گیا" + "ٹاسک بار و بلبلے بائیں طرف ہیں" + "ٹاسک بار و بلبلے دائیں طرف ہیں" "نیویگیشن بار" "ہمیشہ ٹاسک بار دکھائیں" "نیویگیشن موڈ تبدیل کریں" "ٹاسک بار ڈیوائیڈر" + "ٹاسک بار اوورفلو" "اوپر/بائیں طرف منتقل کریں" "نیچے/دائیں طرف منتقل کریں" - "{count,plural, =1{# مزید ایپ دکھائیں۔}other{# مزید ایپس دکھائیں۔}}" - "{count,plural, =1{# ڈیسک ٹاپ ایپ دکھائیں۔}other{# ڈیسک ٹاپ ایپس دکھائیں۔}}" + "ایپ کو بطور ببل کھولیں" + "حالیہ ایپس" + "حالیہ ایپ کی فہرست" + "{count,plural, =1{مزید ایپ}other{مزید ایپس}}" + "ڈیسک ٹاپ" "%1$s اور %2$s" + "‫%1$s، آئٹم %2$d از %3$d" + "بائیں طرف اسکرول کریں" + "دائیں طرف اسکرول کریں" "ببل" "اوورفلو" "%2$s سے %1$s" "%1$s اور %2$d مزید" + "بائیں منتقل کریں" + "دائیں منتقل کریں" + "سبھی کو برخاست کریں" + "%1$s کو پھیلائیں" + "%1$s کو سکیڑیں" + "تلاش کرنے کیلئے دائرہ بنائیں" + "ایپ آئیکن" + "ایپ کا عنوان" + "\'بند کریں\' بٹن" + "ٹاسک بار میں پن کریں" + "ٹاسک بار سے پن ہٹائیں" diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml index 3f4f98109..86fdd5614 100644 --- a/quickstep/res/values-uz/strings.xml +++ b/quickstep/res/values-uz/strings.xml @@ -22,11 +22,13 @@ "Qadash" "Erkin shakl" "Desktop" + "Tashqi displeyga olish" + "Yopish" + "Desktop" "Yaqinda ishlatilgan ilovalar yo‘q" "Ilovadan foydalanish sozlamalari" "Hammasini tozalash" "Oxirgi ilovalar" - "Vazifalar yopildi" "%1$s, %2$s" "< 1 daqiqa" "Bugun %1$s qoldi" @@ -45,6 +47,7 @@ "Ilova tavsiyalari yoqildi" "Endi ilova takliflari chiqmaydi" "Taklif etilgan ilova: %1$s" + "Ishorali navigatsiya darsligi" "Qurilmangizni buring" "Ishorali navigatsiya darsligini tugatish uchun qurilmani buring" "Ekran chetidan boshlab oʻngdan yoki chapdan suring" @@ -56,7 +59,6 @@ "Orqaga ishorasi sezuvchanligi Sozlamalardan oʻzgartiriladi" "Orqaga qaytish" "Ortga qaytish uchun barmoqni ekranning yon chekkalaridan oʻrtasigacha suring." - "Oxirgi ekranga qaytish uchun 2 barmoq bilan ekranning chap yoki oʻng chekkasidan oʻrtasigacha suring." "Orqaga" "Chap yoki oʻng chetidan ekranning oʻrtasiga suring" "Barmoqni ekranning pastki chetidan yuqoriga suring." @@ -66,7 +68,6 @@ "Bosh ekranni ochish ishorasi darsini tamomladingiz" "Svayp bilan bosh ekranni ochish" "Ekranning pastidan tepaga qarab suring. Bu ishora doim Bosh ekranni ochadi." - "2 barmoq bilan ekranning quyidan tepasiga suring. Bu ishora har doim Bosh ekranni ochadi." "Boshiga" "Ekranning quyi qismidan tepaga torting" "Barakalla!" @@ -77,7 +78,6 @@ "Ilovalarni almashtirish darsini tamomladingiz" "Ilovalar orasida almashish" "Ilovalarni ochish uchun ekranning pastidan tepaga qarab suring, biroz ushlab turing va qoʻyib yuboring" - "Ilovalarni almashtirish uchun 2 barmoq bilan ekranning quyidan tepasiga surib turib, qoʻyib yuboring" "Ilovalarni almashtirish" "Ekranning pastidan tepaga qarab suring, biroz ushlab turing va qoʻyib yuboring" "Juda soz!" @@ -90,7 +90,7 @@ "Hammasi tayyor!" "Boshiga qaytish uchun tepaga suring" "Bosh ekranga oʻtish uchun bosh ekran tugmasini bosing" - "%1$s xizmatga tayyor" + "Sizning %1$s xizmatga tayyor" "qurilma" "Tizim navigatsiya sozlamalari" "Ulashish" @@ -99,7 +99,7 @@ "Ilova juftini saqlash" "Ekranni ikkiga ajratish uchun boshqa ilovani bosing" "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" - "Bekor qilish" + "Bekor qilish" "Ekranni ikkiga ajratish tanlovidan chiqish" "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" "Bu amal ilova yoki tashkilotingiz tomonidan taqiqlangan" @@ -130,18 +130,37 @@ "Tezkor sozlamalar" "Vazifalar paneli" "Vazifalar paneli ochiq" - "Vazifalar paneli yopiq" + "Panel va bulutchalar chapda" + "Panel va bulutchalar oʻngda" "Navigatsiya paneli" "Vazifalar paneli doim chiqarilsin" "Navigatsiya rejimini oʻzgartirish" "Vazifalar panelini ajratkich" + "Vazifalar panelini kengaytirish" "Yuqoriga yoki chapga oʻtkazish" "Pastga yoki oʻngga oʻtkazish" - "{count,plural, =1{Yana # ta ilovani chiqarish}other{Yana # ta ilovani chiqarish}}" - "{count,plural, =1{# ta desktop ilovani chiqarish.}other{# ta desktop ilovani chiqarish.}}" + "Ilovani qalqib chiquvchi oynada ochish" + "Oxirgi ilovalar" + "Oxirgi ilovalar roʻyxati" + "{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}" + "Kompyuter" "%1$s va %2$s" + "%1$s, %2$d-element, jami: %3$d ta" + "Chapga varaqlash" + "Oʻngga varaqlash" "Pufak" "Kengaytirish" "%1$s (%2$s)" "%1$s va yana %2$d kishi" + "Chapga siljitish" + "Oʻngga siljitish" + "Hammasini yopish" + "%1$sni yoyish" + "%1$sni yigʻish" + "Chizib qidirish" + "Ilova belgisi" + "Ilova nomi" + "Yopish tugmasi" + "Panelga qadash" + "Vazifalar panelidan yechib olish" diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml index 9bc526faa..26b37702e 100644 --- a/quickstep/res/values-vi/strings.xml +++ b/quickstep/res/values-vi/strings.xml @@ -22,11 +22,13 @@ "Ghim" "Dạng tự do" "Máy tính" + "Chuyển sang màn hình ngoài" + "Đóng" + "Máy tính" "Không có mục gần đây nào" "Cài đặt mức sử dụng ứng dụng" "Xóa tất cả" "Ứng dụng gần đây" - "Đã đóng tác vụ" "%1$s, %2$s" "< 1 phút" "Hôm nay còn %1$s" @@ -45,20 +47,20 @@ "Đã bật tính năng Ứng dụng đề xuất" "Tính năng Ứng dụng đề xuất bị tắt" "Ứng dụng dự đoán: %1$s" + "Hướng dẫn thực hiện thao tác bằng cử chỉ" "Xoay thiết bị của bạn" "Vui lòng xoay thiết bị của bạn để hoàn tất hướng dẫn thao tác bằng cử chỉ" "Hãy vuốt từ mép ngoài cùng bên phải hoặc ngoài cùng bên trái" "Hãy vuốt từ mép phải hoặc mép trái tới giữa màn hình rồi nhấc ngón tay ra" "Bạn đã học được cách vuốt từ mép phải để quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." "Bạn đã thực hiện xong cử chỉ quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." - "Bạn đã thực hiện xong cử chỉ quay lại" + "Bạn đã hoàn tất cử chỉ quay lại" "Hãy nhớ không được vuốt quá gần phần dưới cùng của màn hình" "Để thay đổi độ nhạy của cử chỉ quay lại, hãy vào mục Cài đặt" "Vuốt để quay lại" "Để quay lại màn hình gần đây nhất, hãy vuốt từ mép trái hoặc mép phải tới chính giữa màn hình." - "Để quay lại màn hình trước đó, hãy vuốt 2 ngón tay từ cạnh trái hoặc phải vào giữa màn hình." "Quay lại" - "Hãy vuốt từ mép trái hoặc mép phải tới giữa màn hình" + "Vuốt từ mép trái hoặc mép phải tới giữa màn hình" "Hãy vuốt lên từ mép dưới cùng của màn hình" "Hãy nhớ không được dừng trước khi nhấc ngón tay" "Hãy vuốt thẳng lên" @@ -66,7 +68,6 @@ "Bạn đã thực hiện xong cử chỉ chuyển đến Màn hình chính" "Vuốt để chuyển đến Màn hình chính" "Vuốt lên từ cuối màn hình. Cử chỉ này luôn đưa bạn đến Màn hình chính." - "Vuốt 2 ngón tay lên từ cuối màn hình. Cử chỉ này luôn đưa bạn về Màn hình chính." "Chuyển đến màn hình chính" "Vuốt lên từ mép dưới cùng của màn hình" "Tuyệt vời!" @@ -77,7 +78,6 @@ "Bạn đã thực hiện xong cử chỉ chuyển đổi ứng dụng" "Vuốt để chuyển đổi ứng dụng" "Để chuyển đổi ứng dụng, hãy vuốt lên từ cuối màn hình, giữ rồi thả ra." - "Để chuyển đổi giữa các ứng dụng, hãy vuốt 2 ngón tay lên từ cuối màn hình, giữ rồi thả ra." "Chuyển đổi ứng dụng" "Vuốt lên từ mép dưới cùng của màn hình, giữ rồi nhấc ngón tay ra" "Rất tốt!" @@ -99,7 +99,7 @@ "Lưu cặp ứng dụng" "Nhấn vào ứng dụng khác để chia đôi màn hình" "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" - "Huỷ" + "Huỷ" "Thoát khỏi lựa chọn chia đôi màn hình" "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" "Ứng dụng hoặc tổ chức của bạn không cho phép thực hiện hành động này" @@ -130,18 +130,37 @@ "Cài đặt nhanh" "Thanh tác vụ" "Đã hiện thanh thao tác" - "Đã ẩn thanh thao tác" + "Hiện thanh tác vụ, b.bóng trái" + "Hiện thanh tác vụ, b.bóng phải" "Thanh điều hướng" "Luôn hiện Thanh tác vụ" "Thay đổi chế độ điều hướng" "Đường phân chia Taskbar" + "Trình đơn mục bổ sung trên thanh tác vụ" "Chuyển lên trên cùng/sang bên trái" "Chuyển xuống dưới cùng/sang bên phải" - "{count,plural, =1{Hiện thêm # ứng dụng.}other{Hiện thêm # ứng dụng.}}" - "{count,plural, =1{Hiện # ứng dụng dành cho máy tính.}other{Hiện # ứng dụng dành cho máy tính.}}" + "Mở ứng dụng dưới dạng bong bóng" + "Các ứng dụng gần đây" + "Danh sách ứng dụng gần đây" + "{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}" + "Máy tính" "%1$s và %2$s" + "%1$s, mục %2$d trong số %3$d" + "Cuộn sang trái" + "Cuộn sang phải" "Bong bóng" "Bong bóng bổ sung" "%1$s từ %2$s" "%1$s và %2$d bong bóng khác" + "Di chuyển sang trái" + "Di chuyển sang phải" + "Đóng tất cả" + "mở rộng %1$s" + "thu gọn %1$s" + "Khoanh tròn để tìm kiếm" + "Biểu tượng ứng dụng" + "Tên ứng dụng" + "Nút đóng" + "Ghim vào thanh tác vụ" + "Bỏ ghim khỏi thanh tác vụ" diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml index f6e446e80..21c212f88 100644 --- a/quickstep/res/values-zh-rCN/strings.xml +++ b/quickstep/res/values-zh-rCN/strings.xml @@ -22,11 +22,13 @@ "固定" "自由窗口" "桌面" + "移至外接显示屏" + "关闭" + "桌面设备" "近期没有任何内容" "应用使用设置" "全部清除" "最近用过的应用" - "任务已关闭" "%1$s(%2$s)" "不到 1 分钟" "今天还可使用 %1$s" @@ -45,28 +47,27 @@ "已启用应用建议" "已停用应用建议" "预测的应用:%1$s" + "手势导航教程" "请旋转设备" "请旋转设备,完成手势导航教程" "确保从最右侧或最左侧边缘开始滑动" "确保从右侧或左侧边缘滑动到屏幕中间位置后再松开手指" "您已了解如何使用“从右侧向左滑动”手势返回。接下来学习切换应用吧!" "您完成了“返回”手势教程。接下来了解如何切换应用。" - "您完成了“返回”手势" + "您已完成“返回”手势教程" "确保滑动时手的位置不要太靠近屏幕底部" "如要调节“返回”手势的灵敏度,请转到“设置”" "滑动即可返回" "如要返回上一个屏幕,请从屏幕左侧或右侧边缘往屏幕中间滑动。" - "若要返回上一个屏幕,请用两根手指从屏幕左侧或右侧边缘向中间滑动。" "返回" "从屏幕左侧或右侧边缘滑动到中间" "确保从屏幕底部边缘向上滑动" "松开手指前,请勿中途停顿" "确保笔直向上滑动" - "您完成了“转到主屏幕”手势。接下来了解如何返回。" - "您完成了“转到主屏幕”手势" + "您已完成“前往主屏幕”手势教程。接下来学习如何返回。" + "您已完成“前往主屏幕”手势教程" "上滑可转到主屏幕" "从屏幕底部向上滑动,即可随时回到主屏幕。" - "用两根手指从屏幕底部向上滑动,这个手势会一律使您回到主屏幕。" "前往主屏幕" "从屏幕底部向上滑动" "太棒了!" @@ -74,10 +75,9 @@ "尝试按住窗口较长时间,然后再松开手指" "确保笔直向上滑动,然后停住" "您已了解如何使用手势了。如要关闭手势,请前往“设置”。" - "您完成了应用切换手势" + "您已完成“切换应用”手势教程" "滑动即可切换应用" "如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。" - "如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。" "切换应用" "从屏幕底部向上滑动后按住,然后松开" "恭喜!" @@ -99,7 +99,7 @@ "保存应用组合" "点按另一个应用即可使用分屏" "另外选择一个应用才可使用分屏模式" - "取消" + "取消" "退出分屏选择模式" "另外选择一个应用才可使用分屏模式" "该应用或您所在的单位不允许执行此操作" @@ -130,18 +130,37 @@ "快捷设置" "任务栏" "任务栏已显示" - "任务栏已隐藏" + "已显示任务栏和左侧消息气泡" + "已显示任务栏和右侧消息气泡" "导航栏" "始终显示任务栏" "更改导航模式" "任务栏分隔线" + "任务栏溢出图标" "移到顶部/左侧" "移到底部/右侧" - "{count,plural, =1{显示另外 # 个应用。}other{显示另外 # 个应用。}}" - "{count,plural, =1{显示 # 款桌面应用。}other{显示 # 款桌面应用。}}" + "以气泡的形式打开应用" + "最近打开过的应用" + "“最近打开过的应用”列表" + "{count,plural, =1{多个应用}other{多个应用}}" + "桌面模式" "%1$s和%2$s" + "%1$s,第 %2$d 项(共 %3$d 项)" + "向左滚动" + "向右滚动" "气泡框" "溢出式气泡框" "来自“%2$s”的%1$s" "%1$s以及另外 %2$d 个" + "左移" + "右移" + "全部关闭" + "展开“%1$s”" + "收起“%1$s”" + "圈定即搜" + "应用图标" + "应用名称" + "“关闭”按钮" + "固定到任务栏" + "从任务栏取消固定" diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml index b9d8eb765..34f8aacb4 100644 --- a/quickstep/res/values-zh-rHK/strings.xml +++ b/quickstep/res/values-zh-rHK/strings.xml @@ -22,11 +22,13 @@ "固定" "自由形式" "桌面" + "移至外部顯示屏" + "關閉" + "桌面" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" "最近使用的應用程式" - "閂咗工作" "%1$s,%2$s" "少於 1 分鐘" "今天剩餘時間:%1$s" @@ -45,6 +47,7 @@ "已啟用應用程式建議" "已停用應用程式建議" "預測應用程式:%1$s" + "手勢導覽教學課程" "旋轉裝置方向" "請旋轉裝置方向以完成手勢導覽教學課程" "請確保從螢幕最右側或最左側邊緣滑動" @@ -56,7 +59,6 @@ "如要變更「返回」手勢的敏感度,請前往「設定」" "滑動即可返回" "如要返回上一個畫面,請從螢幕左側或右側邊緣往中央滑動。" - "如要返回上一個畫面,請用兩指從螢幕左側或右側邊緣往中央滑動。" "返回" "從螢幕左側或右側邊緣往中央滑動" "請確保從螢幕底部邊緣向上滑動" @@ -66,7 +68,6 @@ "你已完成「返回主畫面」手勢的教學課程" "向上滑動即可返回主畫面" "從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" - "請用兩指從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" "返回主畫面" "從螢幕底部向上滑動" "太好了!" @@ -77,7 +78,6 @@ "你已完成「切換應用程式」手勢的教學課程" "滑動即可切換應用程式" "如要切換應用程式,請從螢幕底部向上滑動並按住,然後放開。" - "如要切換應用程式,請用兩指從螢幕底部向上滑動並按住,然後放開手指。" "切換應用程式" "從螢幕底部向上滑動並按住,然後放開" "做得好!" @@ -99,7 +99,7 @@ "儲存應用程式組合" "輕按其他應用程式以使用分割螢幕" "選擇其他應用程式才能使用分割螢幕" - "取消" + "取消" "退出分割螢幕選取頁面" "選擇其他應用程式才能使用分割螢幕" "應用程式或你的機構不允許此操作" @@ -130,18 +130,37 @@ "快速設定" "工作列" "顯示咗工作列" - "隱藏咗工作列" + "工作列和對話氣泡在左邊顯示" + "工作列和對話氣泡在右邊顯示" "導覽列" "一律顯示工作列" "變更導覽模式" "工作列分隔線" + "工作列溢位" "移至上方/左側" "移至底部/右側" - "{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個桌面應用程式。}other{顯示 # 個桌面應用程式。}}" + "在小視窗開啟應用程式" + "最近開啟的應用程式" + "「最近開啟的應用程式」清單" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "桌面" "「%1$s」和「%2$s」" + "%1$s,第 %2$d 個項目,總共有 %3$d 項" + "向左捲動" + "向右捲動" "對話氣泡" "展開式" "%2$s 的「%1$s」通知" "%1$s和其他 %2$d 則通知" + "向左移" + "向右移" + "全部關閉" + "打開%1$s" + "收埋%1$s" + "一圈即搜" + "應用程式圖示" + "應用程式名稱" + "關閉按鈕" + "固定至工作列" + "取消固定至工作列" diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml index 90140cb58..5244b05d4 100644 --- a/quickstep/res/values-zh-rTW/strings.xml +++ b/quickstep/res/values-zh-rTW/strings.xml @@ -21,12 +21,14 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "固定" "自由形式" - "電腦" + "電腦模式" + "移至外接螢幕" + "關閉" + "電腦模式" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" "最近使用的應用程式" - "工作已關閉" "%1$s (%2$s)" "< 1 分鐘" "今天還能使用 %1$s" @@ -45,6 +47,7 @@ "應用程式建議功能已啟用" "應用程式建議功能已停用" "預測的應用程式:%1$s" + "手勢操作教學課程" "旋轉裝置螢幕方向" "如要完成手勢操作教學課程,請旋轉裝置螢幕方向" "請務必從螢幕最右側或最左側滑動" @@ -56,7 +59,6 @@ "如要變更「返回」手勢的敏感度,請前往「設定」" "滑動即可返回" "如要返回上一個畫面,請從螢幕左側或右側邊緣往中央滑動。" - "如要返回上一個畫面,請用 2 指從螢幕左側或右側邊緣往中央滑動。" "返回" "從螢幕右側或左側往中央滑動" "請務必從螢幕底部向上滑動" @@ -66,7 +68,6 @@ "你已完成「返回主畫面」手勢的教學課程" "使用滑動手勢返回主畫面" "從螢幕底部向上滑動,即可返回主畫面。" - "用 2 指從螢幕底部向上滑動,即可回到主畫面。" "返回主畫面" "從螢幕底部向上滑動" "太棒了!" @@ -77,7 +78,6 @@ "你已完成「切換應用程式」手勢的教學課程" "使用滑動手勢切換應用程式" "如要切換不同的應用程式,請從螢幕底部向上滑動並按住,然後放開手指。" - "如要切換應用程式,請用 2 指從螢幕底部向上滑動並按住,然後放開手指。" "切換應用程式" "從螢幕底部向上滑動並按住,然後放開手指" "非常好!" @@ -99,7 +99,7 @@ "儲存應用程式配對" "輕觸另一個應用程式即可使用分割畫面" "選擇要在分割畫面中使用的另一個應用程式" - "取消" + "取消" "退出分割畫面選擇器" "必須選擇另一個應用程式才能使用分割畫面" "這個應用程式或貴機構不允許執行這個動作" @@ -130,18 +130,37 @@ "快速設定" "工作列" "已顯示工作列" - "已隱藏工作列" + "工作列和對話框顯示在左側" + "工作列和對話框顯示在右側" "導覽列" "一律顯示工作列" "變更操作模式" "工作列分隔線" + "工作列溢位" "移到上方/左側" "移到底部/右側" - "{count,plural, =1{再多顯示 # 個應用程式。}other{再多顯示 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個電腦版應用程式。}other{顯示 # 個電腦版應用程式。}}" + "以泡泡形式開啟應用程式" + "最近開啟的應用程式" + "「最近開啟的應用程式」清單" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "電腦模式" "「%1$s」和「%2$s」" + "%1$s,第 %2$d 個項目,共 %3$d 項" + "向左捲動" + "向右捲動" "泡泡" "溢位" "「%2$s」的「%1$s」通知" "%1$s和另外 %2$d 則通知" + "向左移" + "向右移" + "全部關閉" + "展開「%1$s」" + "收合「%1$s」" + "畫圈搜尋" + "應用程式圖示" + "應用程式標題" + "關閉按鈕" + "固定到工作列" + "從工作列中取消固定" diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml index 73be445a5..a81fe2567 100644 --- a/quickstep/res/values-zu/strings.xml +++ b/quickstep/res/values-zu/strings.xml @@ -22,11 +22,13 @@ "Phina" "I-Freeform" "Ideskithophu" + "Hambisa esibonisini sangaphandle" + "Vala" + "Ideskithophu" "Azikho izinto zakamuva" "Izilungiselelo zokusetshenziswa kohlelo lokusebenza" "Sula konke" "Izinhlelo zokusebenza zakamuva" - "Umsebenzi Uvaliwe" "%1$s, %2$s" "< 1 iminithi" "%1$s esele namhlanje" @@ -45,6 +47,7 @@ "Iziphakamiso zohlelo lokusebenza zinikwe amandla" "Iziphakamiso zohlelo lokusebenza zikhutshaziwe" "Uhlelo lokusebenza olubikezelwe: %1$s" + "Okokufundisa Kokuzulazula Kokuthinta" "Zungezisa idivayisi yakho" "Sicela uzungezise idivayisi yakho ukuze uqedele okokufundisa kokufuna ngokuthinta" "Qinisekisa ukuthi uswayipha ukusuka onqenqemeni olukude ngakwesokudla noma olukude ngakwesokunxele" @@ -56,7 +59,6 @@ "Ukuze ushintshe ukuzwela kokuthinta emuva, iya Kumasethingi" "Swayipha ukuze uye emuva" "Ukuze ubuyele emuva esikrinini sokugcina, swapha kusuka emngceleni wesobunxele noma wesokudla kuya phakathi kwesikrini." - "Ukuze ubuyele esikrinini sokugcina, swayipha ngeminwe emi-2 ukusuka kwesokunxele noma kwesokudla emphethweni uye phakathi kwesikrini." "Iya emuva" "Swayipha ukusuka kwesokunxele noma kwesokudla ukuya phakathi kwesikrini" "Qiniseka ukuthi uswayiphela phezulu ukusuka emngceleni ophansi wesikrini" @@ -66,7 +68,6 @@ "Ukuqedile ukuthinta kokuya ekhaya" "Swayipha ukuze uye ekhaya" "Swayiphela phezulu kusuka phansi kwesikrini sakho.Lokhu kuthinta kuhlala kukusa esikrinini sasekhaya." - "Swayiphela phezulu ngeminwe emi-2 kusukela phansi esikrinini. Lesi senzo sihlala sikuyisa esikrinini Sasekhaya." "Iya ekhasini lokuqala" "Swayiphela phezulu ukusuka phansi esikrinini sakho" "Umsebenzi omuhle!" @@ -77,7 +78,6 @@ "Ukuqedile ukuthinta kokushintsha ama-app" "Swayipha ukuze ushintshe ama-app" "Ukuze ushintshe phakathi kwama-app, swayiphela phezulu kusuka ngezansi kwesikrini sakho, bese uyadedela." - "Ukuze ushintshe phakathi kwama-app, swayiphela phezulu ngeminwe emi-2 kusukela phansi esikrinini sakho, ubambe, bese uyakhulula." "Shintsha ama-app" "Swayiphela phezulu ukusuka phansi esikrinini sakho, ubambe, bese uyadedela" "Wenze kahle!" @@ -99,7 +99,7 @@ "Londoloza ukubhangqa i-app" "Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa" "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" - "Khansela" + "Khansela" "Phuma ekukhetheni ukuhlukaniswa kwesikrini" "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" "Lesi senzo asivunyelwanga uhlelo lokusebenza noma inhlangano yakho" @@ -130,18 +130,37 @@ "Amasethingi Asheshayo" "I-Taskbar" "Ibha yomsebenzi ibonisiwe" - "Ibha yomsebenzi ifihliwe" + "ITaskbar namabhamuza aboniswe kwesokunxele" + "ITaskbar namabhamuza aboniswe kwesokudla" "Ibha yokufuna" "Bonisa i-Taskbar njalo." "Shintsha imodi yokufuna" "Isihlukanisi se-Taskbar" + "Ukuphuphuma Kwetaskbar" "Hamba phezulu/kwesokunxele" "Hamba phansi/kwesokudla" - "{count,plural, =1{Bonisa i-app e-# ngaphezulu.}one{Bonisa ama-app angu-# ngaphezulu.}other{Bonisa ama-app angu-# ngaphezulu.}}" - "{count,plural, =1{Bonisa i-app engu-# yedeskithophu.}one{Bonisa ama-app angu-# wedeskithophu.}other{Bonisa ama-app angu-# wedeskithophu.}}" + "Vula i-app njengebhamuza" + "Ama-app wakamuva" + "Uhlu lwe-app lwakamuva" + "{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}" + "Ideskithophu" "I-%1$s ne-%2$s" + "I-%1$s, into engu-%2$d kwezingu-%3$d" + "Skrolela ngakwesokunxele" + "Skrolela ngakwesokudla" "Ibhamuza" "Ukugcwala kakhulu" "%1$s kusuka ku-%2$s" "%1$s nokunye okungu-%2$d" + "Iya kwesokunxele" + "Iya kwesokudla" + "Chitha konke" + "nweba %1$s" + "goqa %1$s" + "Khethela Ukusesha" + "Isithonjana se-app" + "Isihloko se-app" + "Inkinobho yokuvala" + "Phina kutaskbar" + "Susa ukuphina i-taskbar" diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml index ccc7f180d..28c0d5c9e 100644 --- a/quickstep/res/values/attrs.xml +++ b/quickstep/res/values/attrs.xml @@ -28,6 +28,7 @@ + @@ -35,6 +36,11 @@ + + + + + - + #fff #39000000 @@ -31,7 +31,6 @@ #99000000 #EBffffff #99000000 - #646464 #ffffff @@ -76,7 +75,7 @@ #80868b #bdc1c6 - #FFFFFFFF + @android:color/system_neutral1_50 #333333 @@ -94,7 +93,5 @@ #f9ab00 - ?androidprv:attr/colorAccentPrimaryVariant - ?androidprv:attr/materialColorPrimaryFixedDim - ?androidprv:attr/materialColorOnPrimaryFixed + @color/materialColorPrimary \ No newline at end of file diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index fd122103e..49cee0fa8 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -23,23 +23,15 @@ com.android.quickstep.logging.StatsLogCompatManager com.android.quickstep.QuickstepTestInformationHandler - com.android.quickstep.util.SystemWindowManagerProxy - com.android.launcher3.uioverrides.QuickstepWidgetHolder$QuickstepHolderFactory com.android.quickstep.InstantAppResolverImpl com.android.launcher3.appprediction.PredictionAppTracker com.android.quickstep.QuickstepProcessInitializer - com.android.launcher3.model.QuickstepModelDelegate com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl com.android.launcher3.taskbar.TaskbarModelCallbacksFactory com.android.launcher3.taskbar.TaskbarViewCallbacksFactory com.android.quickstep.LauncherRestoreEventLoggerImpl - com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl com.android.launcher3.taskbar.TaskbarEduTooltipController - - - - com.android.launcher3.uioverrides.SystemApiWrapper @@ -54,10 +46,17 @@ 23 + 34dp + + + 6 + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 5c82c991c..86d44c978 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -34,13 +34,12 @@ 0.7 - - 0.46 48dp 44dp + + 52dp 4dp @@ -82,6 +81,15 @@ 44dp 4dp + + 30dp + 18dp + 9dp + 18dp + 16dp + + + 25dp 72dp 1.1 @@ -101,6 +109,10 @@ 24dp 2dp + + 56dp + 2dp + 600dp @@ -109,12 +121,14 @@ 0.0285dp 0.15dp 0.285dp + 0.7dp 1.4dp 36dp 0.5dp 115dp + 36dp 100dp @@ -263,7 +277,7 @@ 24dp - 40dp + 40dp 36sp 14sp @@ -348,19 +362,16 @@ 48dp 48dp 32dp - 6dp 6dp + 5.5dp 64dp 64dp 48dp 1dp 72dp - 4dp - 14dp - 2dp - 2dp - 12dp - 2dp + 2dp + 12dp + 4dp 12dp @@ -371,9 +382,11 @@ 10dp 32dp 8dp + 8dp 400dp 8dp 16dp + 4dp 16dp @@ -422,6 +435,10 @@ 300dp 16dp + 16dp + + + 8dp 60dp @@ -434,6 +451,7 @@ 55dp @dimen/transient_taskbar_stashed_height @dimen/taskbar_stashed_handle_height + @dimen/transient_taskbar_stash_spring_velocity_dp_per_s 9dp @@ -443,23 +461,28 @@ 80dp 1dp - 2dp + + 3dp 90dp + 20dp 32dp 36dp + 28dp 24dp 12dp 16dp 6dp 8dp + @dimen/bubblebar_icon_spacing 12dp 1dp + 12dp - 96dp + @dimen/floating_dismiss_background_size 60dp - 24dp + @dimen/floating_dismiss_icon_size 50dp 548dp 192dp @@ -473,31 +496,49 @@ 24dp 16dp + + 16dp + 4dp + 14dp + 238dp + 276dp + 12dp + 10dp + 1dp + 2dp + 20dp - 4dp + 5dp + 3dp 104dp - 134dp + 136dp 52dp 20dp + 32dp 56dp + 24dp 16dp 16dp - 2dp + 4dp 28dp 16dp 24dp 8dp + 104dp + 360dp + 16dp + 20dp + 36dp + 56dp + 6dp + 16dp + 18dp 48dp - - - - 220dp - diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/values/ids.xml similarity index 65% rename from quickstep/res/drawable/bg_circle.xml rename to quickstep/res/values/ids.xml index 506177b6a..c71bb762d 100644 --- a/quickstep/res/drawable/bg_circle.xml +++ b/quickstep/res/values/ids.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 340d25b0f..f263f7efc 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -26,8 +26,15 @@ Pin Freeform - + Desktop + + Move to external display + + Close + + + Desktop No recent items @@ -44,9 +51,6 @@ Recent apps - - Task Closed - %1$s, %2$s @@ -96,6 +100,9 @@ Predicted app: %1$s + + Gesture Navigation Tutorial + Rotate your device @@ -120,8 +127,6 @@ Swipe to go back To go back to the last screen, swipe from the left or right edge to the middle of the screen. - - To go back to the last screen, swipe with 2 fingers from the left or right edge to the middle of the screen. Go back @@ -140,8 +145,6 @@ Swipe to go home Swipe up from the bottom of your screen. This gesture always takes you to the Home screen. - - Swipe up with 2 fingers from the bottom of the screen. This gesture always takes you to the Home screen. Go home @@ -163,8 +166,6 @@ Swipe to switch apps To switch between apps, swipe up from the bottom of your screen, hold, then release. - - To switch between apps, swipe up with 2 fingers from the bottom of your screen, hold, then release. Switch apps @@ -237,7 +238,7 @@ Tap another app to use split screen Choose another app to use split screen - Cancel + Cancel Exit split screen selection Choose another app to use split screen @@ -299,10 +300,12 @@ Quick Settings Taskbar - + Taskbar shown - - Taskbar hidden + + Taskbar & bubbles left shown + + Taskbar & bubbles right shown Navigation bar @@ -311,27 +314,41 @@ Change navigation mode Taskbar Divider + + Taskbar Overflow Move to top/left Move to bottom/right + + Open app as a bubble - + + Recent apps + + + Recent app list + + {count, plural, - =1{Show # more app.} - other{Show # more apps.} + =1{more app} + other{more apps} } - - {count, plural, - =1{Show # desktop app.} - other{Show # desktop apps.} - } + + Desktop %1$s and %2$s + + %1$s, item %2$d of %3$d + + + Scroll left + + Scroll right @@ -342,4 +359,31 @@ %1$s from %2$s %1$s and %2$d more + + Move left + + Move right + + Dismiss all + + expand %1$s + + collapse %1$s + + + Circle to Search + + + + App icon + + App title + + Close button + + + Pin to taskbar + + Unpin from taskbar diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index 952505a48..f8ca8d9ad 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -124,7 +124,7 @@ - Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW) + * |--> (1) Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW) * | | - * | --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW) + * | --> (1a) App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW) * |--> Divider - * |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW) + * |--> (2) Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW) * | - * --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW) + * --> (2a) App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW) * * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones, @@ -682,7 +707,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC launchingIconView: AppPairIcon, transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + windowRadius: Float, ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation @@ -691,7 +717,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo, t, finishCallback, - WINDOWING_MODE_MULTI_WINDOW + WINDOWING_MODE_MULTI_WINDOW, ) return } @@ -702,48 +728,28 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Create an AnimatorSet that will run both shell and launcher transitions together val launchAnimation = AnimatorSet() - var rootCandidate: Change? = null - for (change in transitionInfo.changes) { - val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + val splitRoots: Pair>? = + SplitScreenUtils.extractTopParentAndChildren(transitionInfo) + check(splitRoots != null) { "Could not find split roots" } - // TODO (b/316490565): Replace this logic when SplitBounds is available to - // startAnimation() and we can know the precise taskIds of launching tasks. - // Find a change that has WINDOWING_MODE_MULTI_WINDOW. - if ( - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && - (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) - ) { - // Check if it is a left/top app. - val isLeftTopApp = - (dp.isLeftRightSplit && change.endAbsBounds.left == 0) || - (!dp.isLeftRightSplit && change.endAbsBounds.top == 0) - if (isLeftTopApp) { - // Found one! - rootCandidate = change - break - } - } - } - - // If we could not find a proper root candidate, something went wrong. - check(rootCandidate != null) { "Could not find a split root candidate" } + // Will point to change (0) in diagram above + val mainRootCandidate = splitRoots.first + // Will contain changes (1) and (2) in diagram above + val leafRoots: List = splitRoots.second + // Don't rely on DP.isLeftRightSplit because if launcher is portrait apps could still + // launch in landscape if system auto-rotate is enabled and phone is held horizontally + val isLeftRightSplit = leafRoots.all { it.endAbsBounds.top == 0 } // Find the place where our left/top app window meets the divider (used for the // launcher side animation) + val leftTopApp = + leafRoots.single { change -> + (isLeftRightSplit && change.endAbsBounds.left <= 0) || + (!isLeftRightSplit && change.endAbsBounds.top <= 0) + } val dividerPos = - if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right - else rootCandidate.endAbsBounds.bottom - - // Recurse up the tree until parent is null, then we've found our root. - var parentToken: WindowContainerToken? = rootCandidate.parent - while (parentToken != null) { - rootCandidate = transitionInfo.getChange(parentToken) ?: break - parentToken = rootCandidate.parent - } - - // Make sure nothing weird happened, like getChange() returning null. - check(rootCandidate != null) { "Failed to find a root leash" } + if (isLeftRightSplit) leftTopApp.endAbsBounds.right else leftTopApp.endAbsBounds.bottom // Create a new floating view in Launcher, positioned above the launching icon val drawableArea = launchingIconView.iconDrawableArea @@ -758,13 +764,30 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC drawableArea, appIcon1, appIcon2, - dividerPos + dividerPos, ) floatingView.bringToFront() - launchAnimation.play( - getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate) + val iconLaunchValueAnimator = + getIconLaunchValueAnimator( + t, + dp, + finishCallback, + launcher, + floatingView, + mainRootCandidate, + ) + iconLaunchValueAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator, isReverse: Boolean) { + for (c in leafRoots) { + t.setCornerRadius(c.leash, windowRadius) + t.apply() + } + } + } ) + launchAnimation.play(iconLaunchValueAnimator) launchAnimation.start() } @@ -778,7 +801,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable, - launchFullscreenIndex: Int + launchFullscreenIndex: Int, ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation @@ -787,7 +810,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo, t, finishCallback, - WINDOWING_MODE_FULLSCREEN + WINDOWING_MODE_FULLSCREEN, ) return } @@ -839,7 +862,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC drawableArea, appIcon, null /*appIcon2*/, - 0 /*dividerPos*/ + 0, /*dividerPos*/ ) floatingView.bringToFront() launchAnimation.play( @@ -854,7 +877,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC finishCallback: Runnable, launcher: QuickstepLauncher, floatingView: FloatingAppPairView, - rootCandidate: Change + rootCandidate: Change, ): ValueAnimator { val progressUpdater = ValueAnimator.ofFloat(0f, 1f) val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) @@ -868,7 +891,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.LINEAR, valueAnimator.animatedFraction, timings.appRevealStartOffset, - timings.appRevealEndOffset + timings.appRevealEndOffset, ) // Set the alpha of the shell layer (2 apps + divider) @@ -885,8 +908,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( timings.getStagedRectXInterpolator(), timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mDy = FloatProp( @@ -895,8 +918,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mScaleX = FloatProp( @@ -905,8 +928,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mScaleY = FloatProp( @@ -915,8 +938,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) override fun onUpdate(percent: Float, initOnly: Boolean) { @@ -951,7 +974,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable, - windowingMode: Int + windowingMode: Int, ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) @@ -1037,7 +1060,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC secondTaskId: Int, transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + cornerRadius: Float, ) { var splitRoot1: Change? = null var splitRoot2: Change? = null @@ -1102,7 +1126,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.LINEAR, valueAnimator.animatedFraction, 0.8f, - 1f + 1f, ) for (leash in openingTargets) { animTransaction.setAlpha(leash, progress) @@ -1115,6 +1139,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC override fun onAnimationStart(animation: Animator) { for (leash in openingTargets) { animTransaction.show(leash).setAlpha(leash, 0.0f) + animTransaction.setCornerRadius(leash, cornerRadius) } animTransaction.apply() } diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java index b61854657..3a6d9b027 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java @@ -17,6 +17,7 @@ package com.android.quickstep.util; import static com.android.app.animation.Interpolators.LINEAR; +import static com.android.app.animation.Interpolators.STANDARD; import android.view.animation.Interpolator; @@ -38,6 +39,8 @@ public interface SplitAnimationTimings { int TABLET_APP_PAIR_LAUNCH_DURATION = 998; /** Total duration (ms) for launching an app pair from its icon on phones. */ int PHONE_APP_PAIR_LAUNCH_DURATION = 915; + /** Total duration (ms) for fading out desktop tasks in split mode. */ + int DESKTOP_FADE_OUT_DURATION = 200; // Initialize timing classes so they can be accessed statically SplitAnimationTimings TABLET_OVERVIEW_TO_SPLIT = new TabletOverviewToSplitTimings(); @@ -83,6 +86,10 @@ public interface SplitAnimationTimings { return (float) getStagedRectSlideEnd() / getDuration(); } + default float getDesktopFadeSplitAnimationEndOffset() { + return (float) DESKTOP_FADE_OUT_DURATION / getDuration(); + } + // DEFAULT VALUES: We define default values here so that SplitAnimationTimings can be used // flexibly in animation-running functions, e.g. a single function that handles 2 types of split // animations. The values are not intended to be used, and can safely be removed if refactoring @@ -105,6 +112,10 @@ public interface SplitAnimationTimings { default Interpolator getGridSlidePrimaryInterpolator() { return LINEAR; } default Interpolator getGridSlideSecondaryInterpolator() { return LINEAR; } + default Interpolator getDesktopTaskFadeInterpolator() { + return LINEAR; + } + // Defaults for HomeToSplit default float getScrimFadeInStartOffset() { return 0; } default float getScrimFadeInEndOffset() { return 0; } @@ -120,5 +131,9 @@ public interface SplitAnimationTimings { default float getAppRevealEndOffset() { return 0; } default Interpolator getCellSplitInterpolator() { return LINEAR; } default Interpolator getIconFadeInterpolator() { return LINEAR; } + + default Interpolator getDesktopTaskScaleInterpolator() { + return STANDARD; + } } diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt index 38bbe601b..4005c5a81 100644 --- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt @@ -16,43 +16,78 @@ package com.android.quickstep.util +import android.util.Log +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.TransitionInfo.FLAG_FIRST_CUSTOM import com.android.launcher3.util.SplitConfigurationOptions -import com.android.wm.shell.util.SplitBounds +import com.android.wm.shell.shared.split.SplitBounds +import java.lang.IllegalStateException class SplitScreenUtils { companion object { + private const val TAG = "SplitScreenUtils" + // TODO(b/254378592): Remove these methods when the two classes are reunited /** Converts the shell version of SplitBounds to the launcher version */ @JvmStatic - fun convertShellSplitBoundsToLauncher( - shellSplitBounds: SplitBounds? - ): SplitConfigurationOptions.SplitBounds? { - return if (shellSplitBounds == null) { - null - } else { - SplitConfigurationOptions.SplitBounds( - shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds, - shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId, - shellSplitBounds.snapPosition - ) - } - } + fun convertShellSplitBoundsToLauncher(shellSplitBounds: SplitBounds) = + SplitConfigurationOptions.SplitBounds( + shellSplitBounds.leftTopBounds, + shellSplitBounds.rightBottomBounds, + shellSplitBounds.leftTopTaskId, + shellSplitBounds.rightBottomTaskId, + shellSplitBounds.snapPosition, + ) - /** Converts the launcher version of SplitBounds to the shell version */ - @JvmStatic - fun convertLauncherSplitBoundsToShell( - launcherSplitBounds: SplitConfigurationOptions.SplitBounds? - ): SplitBounds? { - return if (launcherSplitBounds == null) { - null + /** + * Given a TransitionInfo, generates the tree structure for those changes and extracts out + * the top most root and it's two immediate children. Changes can be provided in any order. + * + * @return a [Pair] where first -> top most split root, second -> [List] of 2, + * leftTop/bottomRight stage roots + */ + fun extractTopParentAndChildren( + transitionInfo: TransitionInfo + ): Pair>? { + val parentToChildren = mutableMapOf>() + val hasParent = mutableSetOf() + // filter out anything that isn't opening and the divider + val taskChanges: List = + transitionInfo.changes + .filter { change -> + (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) && + change.flags < FLAG_FIRST_CUSTOM + } + .toList() + + // 1. Build Parent-Child Relationships + for (change in taskChanges) { + // TODO (b/316490565): Replace this logic when SplitBounds is available to + // startAnimation() and we can know the precise taskIds of launching tasks. + change.parent?.let { parent -> + parentToChildren + .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() } + .add(change) + hasParent.add(change) + } + } + + // 2. Find Top Parent + val topParent = taskChanges.firstOrNull { it !in hasParent } + + // 3. Extract Immediate Children + return if (topParent != null) { + val immediateChildren = parentToChildren.getOrDefault(topParent, emptyList()) + if (immediateChildren.size != 2) { + throw IllegalStateException("incorrect split stage root size") + } + Pair(topParent, immediateChildren) } else { - SplitBounds( - launcherSplitBounds.leftTopBounds, - launcherSplitBounds.rightBottomBounds, - launcherSplitBounds.leftTopTaskId, - launcherSplitBounds.rightBottomTaskId, - launcherSplitBounds.snapPosition - ) + Log.w(TAG, "No top parent found") + null } } } diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index 7e7c79430..08f255215 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; -import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP; @@ -35,8 +34,8 @@ import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; -import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -53,16 +52,13 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; -import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; import android.util.Pair; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.View; import android.window.IRemoteTransitionFinishedCallback; import android.window.RemoteTransition; import android.window.RemoteTransitionStub; @@ -75,12 +71,12 @@ import com.android.internal.logging.InstanceId; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.apppairs.AppPairIcon; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconProvider; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -94,23 +90,22 @@ import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsModel; import com.android.quickstep.SplitSelectionListener; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.SplitInstructionsView; -import com.android.systemui.animation.RemoteAnimationRunnerCompat; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.systemui.shared.system.QuickStepContract; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.splitscreen.ISplitSelectListener; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; /** @@ -121,7 +116,6 @@ public class SplitSelectStateController { private static final String TAG = "SplitSelectStateCtor"; private RecentsViewContainer mContainer; - private final Handler mHandler; private final RecentsModel mRecentTasksModel; @Nullable private Runnable mActivityBackCallback; @@ -154,9 +148,10 @@ public class SplitSelectStateController { /** * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be - * set for all launches. + * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below. */ private int mLaunchCuj = -1; + private View mLaunchingViewCuj; private FloatingTaskView mFirstFloatingTaskView; private SplitInstructionsView mSplitInstructionsView; @@ -168,10 +163,12 @@ public class SplitSelectStateController { */ private Pair mSessionInstanceIds; + private boolean mIsDestroyed = false; + private final BackPressHandler mSplitBackHandler = new BackPressHandler() { @Override public boolean canHandleBack() { - return FeatureFlags.enableSplitContextually() && isSplitSelectActive(); + return isSplitSelectActive(); } @Override @@ -186,12 +183,11 @@ public class SplitSelectStateController { } }; - public SplitSelectStateController(RecentsViewContainer container, Handler handler, + public SplitSelectStateController(RecentsViewContainer container, StateManager stateManager, DepthController depthController, StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, Runnable activityBackCallback) { mContainer = container; - mHandler = handler; mStatsLogManager = statsLogManager; mSystemUiProxy = systemUiProxy; mStateManager = stateManager; @@ -199,12 +195,13 @@ public class SplitSelectStateController { mRecentTasksModel = recentsModel; mActivityBackCallback = activityBackCallback; mSplitAnimationController = new SplitAnimationController(this); - mAppPairsController = new AppPairsController(mContainer.asContext(), this, statsLogManager); + mAppPairsController = new AppPairsController(mContainer, this, statsLogManager); mSplitSelectDataHolder = new SplitSelectDataHolder(mContainer.asContext()); } public void onDestroy() { mContainer = null; + mIsDestroyed = true; mActivityBackCallback = null; mAppPairsController.onDestroy(); mSplitSelectDataHolder.onDestroy(); @@ -262,7 +259,7 @@ public class SplitSelectStateController { GroupTask groupTask = taskGroups.get(i); if (isInstanceOfAppPair( groupTask, componentKeys.get(0), componentKeys.get(1))) { - lastActiveTasks[0] = groupTask.task1; + lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask(); break; } } @@ -275,17 +272,15 @@ public class SplitSelectStateController { // Loop through tasks in reverse, since they are ordered with recent tasks last for (int j = taskGroups.size() - 1; j >= 0; j--) { GroupTask groupTask = taskGroups.get(j); - Task task1 = groupTask.task1; - // Don't add duplicate Tasks - if (isInstanceOfComponent(task1, key) - && !Arrays.asList(lastActiveTasks).contains(task1)) { - lastActiveTask = task1; - break; + // Account for desktop cases where there can be N tasks in the group + for (Task task : groupTask.getTasks()) { + if (isInstanceOfComponent(task, key) + && !Arrays.asList(lastActiveTasks).contains(task)) { + lastActiveTask = task; + break; + } } - Task task2 = groupTask.task2; - if (isInstanceOfComponent(task2, key) - && !Arrays.asList(lastActiveTasks).contains(task2)) { - lastActiveTask = task2; + if (lastActiveTask != null) { break; } } @@ -318,11 +313,15 @@ public class SplitSelectStateController { */ public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1, @NonNull ComponentKey componentKey2) { - return ((isInstanceOfComponent(groupTask.task1, componentKey1) - && isInstanceOfComponent(groupTask.task2, componentKey2)) - || - (isInstanceOfComponent(groupTask.task1, componentKey2) - && isInstanceOfComponent(groupTask.task2, componentKey1))); + if (groupTask instanceof SplitTask splitTask) { + return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1) + && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2)) + || + (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2) + && isInstanceOfComponent(splitTask.getBottomRightTask(), + componentKey1))); + } + return false; } /** @@ -372,7 +371,7 @@ public class SplitSelectStateController { * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio. */ public void launchSplitTasks(@Nullable Consumer callback) { - launchSplitTasks(SNAP_TO_50_50, callback); + launchSplitTasks(SNAP_TO_2_50_50, callback); } /** @@ -380,7 +379,7 @@ public class SplitSelectStateController { * ratio and no callback. */ public void launchSplitTasks() { - launchSplitTasks(SNAP_TO_50_50, null); + launchSplitTasks(SNAP_TO_2_50_50, null); } /** @@ -459,77 +458,41 @@ public class SplitSelectStateController { Bundle optionsBundle = options1.toBundle(); Bundle extrasBundle = new Bundle(1); extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent); - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, - secondTaskId, callback, "LaunchSplitPair"); - switch (launchData.getSplitLaunchType()) { - case SPLIT_TASK_TASK -> - mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, - null /* options2 */, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId, + secondTaskId, callback, "LaunchSplitPair"); + switch (launchData.getSplitLaunchType()) { + case SPLIT_TASK_TASK -> + mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, + null /* options2 */, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_TASK_PENDINGINTENT -> - mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, - firstTaskId, extrasBundle, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_TASK_PENDINGINTENT -> + mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, + firstTaskId, extrasBundle, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_TASK_SHORTCUT -> - mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, - firstTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_TASK_SHORTCUT -> + mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, + firstTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_PENDINGINTENT_TASK -> - mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, - secondTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_PENDINGINTENT_TASK -> + mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, + secondTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_PENDINGINTENT_PENDINGINTENT -> - mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, - optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle, - initialStagePosition, snapPosition, remoteTransition, - shellInstanceId); + case SPLIT_PENDINGINTENT_PENDINGINTENT -> + mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, + optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle, + initialStagePosition, snapPosition, remoteTransition, + shellInstanceId); - case SPLIT_SHORTCUT_TASK -> - mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, - secondTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); - } - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId, - callback); - switch (launchData.getSplitLaunchType()) { - case SPLIT_TASK_TASK -> - mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, - secondTaskId, null /* options2 */, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_TASK_PENDINGINTENT -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI, - secondUserId, optionsBundle, firstTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - - case SPLIT_TASK_SHORTCUT -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut, - optionsBundle, firstTaskId, null /*options2*/, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_PENDINGINTENT_TASK -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - - case SPLIT_PENDINGINTENT_PENDINGINTENT -> - mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstUserId, - firstShortcut, optionsBundle, secondPI, secondUserId, - secondShortcut, null /*options2*/, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_SHORTCUT_TASK -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - } + case SPLIT_SHORTCUT_TASK -> + mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, + secondTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); } + } /** @@ -575,20 +538,13 @@ public class SplitSelectStateController { } Bundle optionsBundle = options1.toBundle(); - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteTransition transition = remoteTransition == null - ? getShellRemoteTransition( - firstTaskId, secondTaskId, callback, "LaunchExistingPair") - : remoteTransition; - mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, - stagePosition, snapPosition, transition, null /*shellInstanceId*/); - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, - secondTaskId, callback); - mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, secondTaskId, - null /* options2 */, stagePosition, snapPosition, adapter, - null /*shellInstanceId*/); - } + final RemoteTransition transition = remoteTransition == null + ? getRemoteTransition( + firstTaskId, secondTaskId, callback, "LaunchExistingPair") + : remoteTransition; + mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, + stagePosition, snapPosition, transition, null /*shellInstanceId*/); + } /** @@ -614,44 +570,24 @@ public class SplitSelectStateController { ActivityThread.currentActivityThread().getApplicationThread(), "LaunchAppFullscreen"); InstanceId instanceId = mSessionInstanceIds.first; - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - switch (launchData.getSplitLaunchType()) { - case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, - optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, - SNAP_TO_50_50, remoteTransition, instanceId); - case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, - firstUserId, optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId); - case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( - initialShortcut, optionsBundle, firstTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId); - } - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, - secondTaskId, callback); - switch (launchData.getSplitLaunchType()) { - case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition( - firstTaskId, optionsBundle, secondTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - case SPLIT_SINGLE_INTENT_FULLSCREEN -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition( - initialShortcut, optionsBundle, firstTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - } + switch (launchData.getSplitLaunchType()) { + case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, + optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, + SNAP_TO_2_50_50, remoteTransition, instanceId); + case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, + firstUserId, optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); + case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( + initialShortcut, optionsBundle, firstTaskId, null /* options2 */, + initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); } } /** * Init {@code SplitFromDesktopController} */ - public void initSplitFromDesktopController(QuickstepLauncher launcher, - OverviewComponentObserver overviewComponentObserver) { - initSplitFromDesktopController( - new SplitFromDesktopController(launcher, overviewComponentObserver)); + public void initSplitFromDesktopController(QuickstepLauncher launcher) { + initSplitFromDesktopController(new SplitFromDesktopController(launcher)); } @VisibleForTesting @@ -659,7 +595,7 @@ public class SplitSelectStateController { mSplitFromDesktopController = controller; } - private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId, + private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId, @Nullable Consumer callback, String transitionName) { final RemoteSplitLaunchTransitionRunner animationRunner = new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); @@ -667,14 +603,6 @@ public class SplitSelectStateController { ActivityThread.currentActivityThread().getApplicationThread(), transitionName); } - private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId, - @Nullable Consumer callback) { - final RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); - return new RemoteAnimationAdapter(animationRunner, 300, 150, - ActivityThread.currentActivityThread().getApplicationThread()); - } - /** * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from * {@link #mSplitSelectDataHolder} @@ -727,7 +655,12 @@ public class SplitSelectStateController { return mSplitAnimationController; } - public void setLaunchingCuj(int launchCuj) { + /** + * Set params to invoke a trace session for the given view and CUJ when we begin animating the + * split launch AFTER we get a response from Shell. + */ + public void setLaunchingCuj(View launchingView, int launchCuj) { + mLaunchingViewCuj = launchingView; mLaunchCuj = launchCuj; } @@ -748,7 +681,7 @@ public class SplitSelectStateController { } @Override - public void startAnimation(IBinder transition, TransitionInfo info, + public void startAnimation(IBinder transition, TransitionInfo transitionInfo, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { final Runnable finishAdapter = () -> { @@ -765,6 +698,9 @@ public class SplitSelectStateController { && mLaunchingTaskView.getRecentsView() != null && mLaunchingTaskView.getRecentsView().isTaskViewVisible( mLaunchingTaskView); + if (mLaunchingViewCuj != null && mLaunchCuj != -1) { + InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj); + } mSplitAnimationController.playSplitLaunchAnimation( shouldLaunchFromTaskView ? mLaunchingTaskView : null, mLaunchingIconView, @@ -775,10 +711,11 @@ public class SplitSelectStateController { null /* nonApps */, mStateManager, mDepthController, - info, t, () -> { + transitionInfo, t, () -> { finishAdapter.run(); cleanup(true /*success*/); - }); + }, + QuickStepContract.getWindowCornerRadius(mContainer.asContext())); }); } @@ -805,60 +742,15 @@ public class SplitSelectStateController { } /** - * LEGACY - * Remote animation runner for animation to launch an app. - */ - private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat { - - private final int mInitialTaskId; - private final int mSecondTaskId; - private final Consumer mSuccessCallback; - - RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, - @Nullable Consumer successCallback) { - mInitialTaskId = initialTaskId; - mSecondTaskId = secondTaskId; - mSuccessCallback = successCallback; - } - - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - Runnable finishedCallback) { - postAsyncCallback(mHandler, - () -> mSplitAnimationController.playSplitLaunchAnimation(mLaunchingTaskView, - mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers, - nonApps, mStateManager, mDepthController, null /* info */, null /* t */, - () -> { - finishedCallback.run(); - if (mSuccessCallback != null) { - mSuccessCallback.accept(true); - } - resetState(); - })); - } - - @Override - public void onAnimationCancelled() { - postAsyncCallback(mHandler, () -> { - if (mSuccessCallback != null) { - // Launching legacy tasks while recents animation is running will always cause - // onAnimationCancelled to be called (should be fixed w/ shell transitions?) - mSuccessCallback.accept(mRecentsAnimationRunning); - } - resetState(); - }); - } - } - - /** - * To be called whenever we exit split selection state. If - * {@link FeatureFlags#enableSplitContextually()} is set, this should be the + * To be called whenever we exit split selection state. This should be the * central way split is getting reset, which should then go through the callbacks to reset * other state. */ public void resetState() { mSplitSelectDataHolder.resetState(); + if (!mIsDestroyed) { + mContainer.getOverviewPanel().resetDesktopTaskFromSplitSelectState(); + } dispatchOnSplitSelectionExit(); mRecentsAnimationRunning = false; mLaunchingTaskView = null; @@ -873,6 +765,7 @@ public class SplitSelectStateController { InteractionJankMonitorWrapper.end(mLaunchCuj); } mLaunchCuj = -1; + mLaunchingViewCuj = null; if (mSessionInstanceIds != null) { mStatsLogManager.logger() @@ -957,32 +850,24 @@ public class SplitSelectStateController { private final int mSplitPlaceholderSize; private final int mSplitPlaceholderInset; private ActivityManager.RunningTaskInfo mTaskInfo; - private ISplitSelectListener mSplitSelectListener; + private DesktopSplitSelectListenerImpl mSplitSelectListener; private Drawable mAppIcon; - public SplitFromDesktopController(QuickstepLauncher launcher, - OverviewComponentObserver overviewComponentObserver) { + public SplitFromDesktopController(QuickstepLauncher launcher) { mLauncher = launcher; - mOverviewComponentObserver = overviewComponentObserver; + mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher); mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_size); mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_inset); - mSplitSelectListener = new ISplitSelectListener.Stub() { - @Override - public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, - int splitPosition, Rect taskBounds) { - MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition, - taskBounds)); - return true; - } - }; + mSplitSelectListener = new DesktopSplitSelectListenerImpl(this); SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener); } void onDestroy() { SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener( mSplitSelectListener); + mSplitSelectListener.release(); mSplitSelectListener = null; } @@ -1005,20 +890,22 @@ public class SplitSelectStateController { Log.w(TAG, "Package not found: " + packageName, e); } RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( - SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), - false /* allowMinimizeSplitScreen */); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())); DesktopSplitRecentsAnimationListener listener = new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds); - MAIN_EXECUTOR.execute(() -> { - callbacks.addListener(listener); - UI_HELPER_EXECUTOR.execute( - // Transition from app to enter stage split in launcher with - // recents animation. - () -> ActivityManagerWrapper.getInstance().startRecentsActivity( - mOverviewComponentObserver.getOverviewIntent(), - SystemClock.uptimeMillis(), callbacks, null, null)); + callbacks.addListener(listener); + UI_HELPER_EXECUTOR.execute(() -> { + // Transition from app to enter stage split in launcher with recents animation + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + options.setTransientLaunch(); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) + .startRecentsActivity( + mOverviewComponentObserver.getOverviewIntent(), options, + callbacks, false /* useSyntheticRecentsTransition */); }); } @@ -1035,7 +922,7 @@ public class SplitSelectStateController { @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { StatsLogManager.LauncherEvent launcherDesktopSplitEvent = mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ? LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM : @@ -1054,6 +941,10 @@ public class SplitSelectStateController { mLauncher, mLauncher.getDragLayer(), null /* thumbnail */, mAppIcon, new RectF()); + floatingTaskView.setOnClickListener(view -> + getSplitAnimationController() + .playAnimPlaceholderToFullscreen(mContainer, view, + Optional.of(() -> resetState()))); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); @@ -1062,7 +953,16 @@ public class SplitSelectStateController { anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - controller.finish(true /* toRecents */, null /* onFinishComplete */, + controller.finish( + true /* toRecents */, + () -> { + LauncherTaskbarUIController controller = + mLauncher.getTaskbarUIController(); + if (controller != null) { + controller.updateTaskbarLauncherStateGoingHome(); + } + + }, false /* sendUserLeaveHint */); } @Override @@ -1070,9 +970,49 @@ public class SplitSelectStateController { SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) .onDesktopSplitSelectAnimComplete(mTaskInfo); } + @Override + public void onAnimationCancel(Animator animation) { + mLauncher.getDragLayer().removeView(floatingTaskView); + getSplitAnimationController() + .removeSplitInstructionsView(mLauncher); + resetState(); + } }); + anim.add(getSplitAnimationController() + .getShowSplitInstructionsAnim(mLauncher).buildAnim()); anim.buildAnim().start(); } } } + + /** + * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher + * activity via the controller. + */ + private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub { + + private SplitFromDesktopController mController; + + DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) { + mController = controller; + } + + /** + * Clears any references to the controller. + */ + void release() { + mController = null; + } + + @Override + public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, + int splitPosition, Rect taskBounds) { + MAIN_EXECUTOR.execute(() -> { + if (mController != null) { + mController.enterSplitSelect(taskInfo, splitPosition, taskBounds); + } + }); + return true; + } + } } diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index 4962367bd..d3390b4ed 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.animation.Animator; @@ -48,8 +49,11 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import java.util.Collections; + /** Handles when the stage split lands on the home screen. */ public class SplitToWorkspaceController { @@ -86,7 +90,7 @@ public class SplitToWorkspaceController { MODEL_EXECUTOR.execute(() -> { PackageItemInfo infoInOut = new PackageItemInfo(pendingIntent.getCreatorPackage(), pendingIntent.getCreatorUserHandle()); - mIconCache.getTitleAndIconForApp(infoInOut, false); + mIconCache.getTitleAndIconForApp(infoInOut, DEFAULT_LOOKUP_FLAG); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); view.post(() -> { @@ -133,10 +137,20 @@ public class SplitToWorkspaceController { // Use Launcher's default click handler return false; } - - mController.setSecondTask(intent, user, (ItemInfo) tag); - - startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher)); + // Check for background task matching this tag; if we find one, set second task + // via task instead of intent so the bounds and windowing mode will be corrected. + mController.findLastActiveTasksAndRunCallback( + Collections.singletonList(((ItemInfo) tag).getComponentKey()), + false /* findExactPairMatch */, + foundTasks -> { + Task foundTask = foundTasks[0]; + if (foundTask != null) { + mController.setSecondTask(foundTask, (ItemInfo) tag); + } else { + mController.setSecondTask(intent, user, (ItemInfo) tag); + } + startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher)); + }); return true; } @@ -205,6 +219,6 @@ public class SplitToWorkspaceController { } private boolean shouldIgnoreSecondSplitLaunch() { - return !FeatureFlags.enableSplitContextually() || !mController.isSplitSelectActive(); + return !mController.isSplitSelectActive(); } } diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java index 5e42b9001..0e2713981 100644 --- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; -import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; @@ -27,9 +26,10 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITIO import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.graphics.Rect; import android.graphics.RectF; -import android.os.SystemClock; +import android.window.TransitionInfo; import androidx.annotation.BinderThread; @@ -40,7 +40,6 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.RecentsAnimationController; -import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; @@ -55,20 +54,16 @@ public class SplitWithKeyboardShortcutController { private final QuickstepLauncher mLauncher; private final SplitSelectStateController mController; - private final RecentsAnimationDeviceState mDeviceState; private final OverviewComponentObserver mOverviewComponentObserver; private final int mSplitPlaceholderSize; private final int mSplitPlaceholderInset; - public SplitWithKeyboardShortcutController(QuickstepLauncher launcher, - SplitSelectStateController controller, - OverviewComponentObserver overviewComponentObserver, - RecentsAnimationDeviceState deviceState) { + public SplitWithKeyboardShortcutController( + QuickstepLauncher launcher, SplitSelectStateController controller) { mLauncher = launcher; mController = controller; - mDeviceState = deviceState; - mOverviewComponentObserver = overviewComponentObserver; + mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher); mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_size); @@ -78,49 +73,48 @@ public class SplitWithKeyboardShortcutController { @BinderThread public void enterStageSplit(boolean leftOrTop) { - if (!enableSplitContextually() || - // Do not enter stage split from keyboard shortcuts if the user is already in split - TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) { + if (TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) { + // Do not enter stage split from keyboard shortcuts if the user is already in split return; } RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( - SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), - false /* allowMinimizeSplitScreen */); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())); SplitWithKeyboardShortcutRecentsAnimationListener listener = new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop); MAIN_EXECUTOR.execute(() -> { callbacks.addListener(listener); - UI_HELPER_EXECUTOR.execute( - // Transition from fullscreen app to enter stage split in launcher with - // recents animation. - () -> ActivityManagerWrapper.getInstance().startRecentsActivity( - mOverviewComponentObserver.getOverviewIntent(), - SystemClock.uptimeMillis(), callbacks, null, null)); + UI_HELPER_EXECUTOR.execute(() -> { + // Transition from fullscreen app to enter stage split in launcher with + // recents animation + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + options.setTransientLaunch(); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) + .startRecentsActivity(mOverviewComponentObserver.getOverviewIntent(), + ActivityOptions.makeBasic(), callbacks, + false /* useSyntheticRecentsTransition */); + }); }); } - public void onDestroy() { - mOverviewComponentObserver.onDestroy(); - mDeviceState.destroy(); - } - private class SplitWithKeyboardShortcutRecentsAnimationListener implements RecentsAnimationCallbacks.RecentsAnimationListener { private final boolean mLeftOrTop; private final Rect mTempRect = new Rect(); + private final ActivityManager.RunningTaskInfo mRunningTaskInfo; private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) { mLeftOrTop = leftOrTop; + mRunningTaskInfo = ActivityManagerWrapper.getInstance().getRunningTask(); } @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { - ActivityManager.RunningTaskInfo runningTaskInfo = - ActivityManagerWrapper.getInstance().getRunningTask(); - mController.setInitialTaskSelect(runningTaskInfo, + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { + mController.setInitialTaskSelect(mRunningTaskInfo, mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT, null /* itemInfo */, mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP @@ -136,14 +130,15 @@ public class SplitWithKeyboardShortcutController { RectF startingTaskRect = new RectF(); final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( mLauncher, mLauncher.getDragLayer(), - controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(), + controller.screenshotTask(mRunningTaskInfo.taskId).getThumbnail(), null /* icon */, startingTaskRect); + Task task = Task.from(new Task.TaskKey(mRunningTaskInfo), mRunningTaskInfo, + false /* isLocked */); RecentsModel.INSTANCE.get(mLauncher.getApplicationContext()) .getIconCache() - .updateIconInBackground( - Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo, - false /* isLocked */), - (task) -> floatingTaskView.setIcon(task.icon)); + .getIconInBackground( + task, + (icon, contentDescription, title) -> floatingTaskView.setIcon(icon)); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java index 997a842dc..a2856a601 100644 --- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java +++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java @@ -49,6 +49,7 @@ import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DynamicResource; import com.android.quickstep.views.RecentsView; import com.android.systemui.plugins.ResourceProvider; @@ -63,8 +64,7 @@ public class StaggeredWorkspaceAnim { private static final int APP_CLOSE_ROW_START_DELAY_MS = 10; // Should be used for animations running alongside this StaggeredWorkspaceAnim. public static final int DURATION_MS = 250; - public static final int DURATION_TASKBAR_MS = - QuickstepTransitionManager.getTaskbarToHomeDuration(); + private final int mTaskbarDurationInMs; private static final float MAX_VELOCITY_PX_PER_S = 22f; @@ -81,6 +81,10 @@ public class StaggeredWorkspaceAnim { public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity, boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) { + boolean isPinnedTaskbarAndNotInDesktopMode = DisplayController.isPinnedTaskbar(launcher) + && !DisplayController.isInDesktopMode(launcher); + mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration( + isPinnedTaskbarAndNotInDesktopMode); prepareToAnimate(launcher, animateOverviewScrim); mIgnoredView = ignoredView; @@ -93,7 +97,7 @@ public class StaggeredWorkspaceAnim { .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y); DeviceProfile grid = launcher.getDeviceProfile(); - long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS; + long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS; if (staggerWorkspace) { Workspace workspace = launcher.getWorkspace(); Hotseat hotseat = launcher.getHotseat(); diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java index e44f14819..54ac1cfa3 100644 --- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java +++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java @@ -18,6 +18,7 @@ package com.android.quickstep.util; import android.animation.Animator; import android.animation.RectEvaluator; +import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -25,6 +26,7 @@ import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; +import android.util.Rational; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; @@ -39,7 +41,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.quickstep.TaskAnimationManager; import com.android.systemui.shared.pip.PipSurfaceTransactionHelper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.pip.PipContentOverlay; +import com.android.wm.shell.shared.pip.PipContentOverlay; /** * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window @@ -50,8 +52,6 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private static final float END_PROGRESS = 1.0f; - private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f; - private final int mTaskId; private final ActivityInfo mActivityInfo; private final SurfaceControl mLeash; @@ -137,8 +137,13 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { mDestinationBoundsTransformed.set(destinationBoundsTransformed); mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius); - final float aspectRatio = destinationBounds.width() / (float) destinationBounds.height(); + final Rational aspectRatio = new Rational( + destinationBounds.width(), destinationBounds.height()); String reasonForCreateOverlay = null; // For debugging purpose. + + // Slightly larger app bounds to allow for off by 1 pixel source-rect-hint errors. + Rect overflowAppBounds = new Rect(appBounds.left - 1, appBounds.top - 1, + appBounds.right + 1, appBounds.bottom + 1); if (sourceRectHint.isEmpty()) { reasonForCreateOverlay = "Source rect hint is empty"; } else if (sourceRectHint.width() < destinationBounds.width() @@ -149,40 +154,27 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { // animation in this case. reasonForCreateOverlay = "Source rect hint is too small " + sourceRectHint; sourceRectHint.setEmpty(); - } else if (!appBounds.contains(sourceRectHint)) { + } else if (!overflowAppBounds.contains(sourceRectHint)) { // This is a situation in which the source hint rect is outside the app bounds, so it is // not a valid rectangle to use for cropping app surface reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint; sourceRectHint.setEmpty(); - } else if (Math.abs( - aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height())) - > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) { - // The source rect hint does not aspect ratio - reasonForCreateOverlay = "Source rect hint does not match aspect ratio " - + sourceRectHint + " aspect ratio " + aspectRatio; - sourceRectHint.setEmpty(); + } else { + final Rational srcAspectRatio = new Rational( + sourceRectHint.width(), sourceRectHint.height()); + if (!PictureInPictureParams.isSameAspectRatio(destinationBounds, srcAspectRatio)) { + // The aspect ratio of destination bounds does not match source rect hint. + // We use the aspect ratio of source rect hint to check against destination bounds + // here to avoid upscaling error. + reasonForCreateOverlay = "Source rect hint:" + sourceRectHint + + " does not match destination bounds:" + destinationBounds; + sourceRectHint.setEmpty(); + } } if (sourceRectHint.isEmpty()) { - // Crop a Rect matches the aspect ratio and pivots at the center point. - // To make the animation path simplified. - if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) { - // use the full height. - mSourceRectHint.set(0, 0, - (int) (appBounds.height() * aspectRatio), appBounds.height()); - mSourceRectHint.offset( - (appBounds.width() - mSourceRectHint.width()) / 2, 0); - } else { - // use the full width. - mSourceRectHint.set(0, 0, - appBounds.width(), (int) (appBounds.width() / aspectRatio)); - mSourceRectHint.offset( - 0, (appBounds.height() - mSourceRectHint.height()) / 2); - } - - // Create a new overlay layer. We do not call detach on this instance, it's propagated - // to other classes like PipTaskOrganizer / RecentsAnimationController to complete - // the cleanup. + mSourceRectHint.set( + getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio.floatValue())); mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(), mAppBounds, mDestinationBounds, new IconProvider(context).getIcon(mActivityInfo), appIconSizePx); @@ -225,6 +217,26 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { addOnUpdateListener(this::onAnimationUpdate); } + /** + * Crop a Rect matches the aspect ratio and pivots at the center point. + */ + private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) { + final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height(); + final int width, height; + int left = appBounds.left; + int top = appBounds.top; + if (appBoundsAspectRatio < aspectRatio) { + width = appBounds.width(); + height = (int) (width / aspectRatio); + top = appBounds.top + (appBounds.height() - height) / 2; + } else { + height = appBounds.height(); + width = (int) (height * aspectRatio); + left = appBounds.left + (appBounds.width() - width) / 2; + } + return new Rect(left, top, left + width, top + height); + } + private void onAnimationUpdate(RectF currentRect, float progress) { if (mHasAnimationEnded) return; final SurfaceControl.Transaction tx = @@ -437,13 +449,22 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { return this; } + public Builder setDisplayCutoutInsets(@NonNull Rect displayCutoutInsets) { + mDisplayCutoutInsets = new Rect(displayCutoutInsets); + return this; + } + public SwipePipToHomeAnimator build() { if (mDestinationBoundsTransformed.isEmpty()) { mDestinationBoundsTransformed.set(mDestinationBounds); } // adjust the mSourceRectHint / mAppBounds by display cutout if applicable. if (mSourceRectHint != null && mDisplayCutoutInsets != null) { - if (mFromRotation == Surface.ROTATION_90) { + if (mFromRotation == Surface.ROTATION_0) { + // TODO: this is to special case the issues on Foldable device + // with display cutout. + mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); + } else if (mFromRotation == Surface.ROTATION_90) { mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); } else if (mFromRotation == Surface.ROTATION_270) { mAppBounds.inset(mDisplayCutoutInsets); @@ -457,15 +478,6 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { } } - private static class RotatedPosition { - private final float degree; - private final float positionX; - private final float positionY; - - private RotatedPosition(float degree, float positionX, float positionY) { - this.degree = degree; - this.positionX = positionX; - this.positionY = positionY; - } + private record RotatedPosition(float degree, float positionX, float positionY) { } } diff --git a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt index 5f4388c91..1ff05dae5 100644 --- a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt @@ -47,6 +47,22 @@ object SystemUiFlagUtils { !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY) } + /** + * Taskbar is hidden whenever the device is dreaming. The dreaming state includes the + * interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in when + * the device is asleep, the second condition extends ensures that the transition from and to + * the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar hide/reveal + * animation timings. The Taskbar can show when dreaming if the glanceable hub is showing on + * top. + */ + @JvmStatic + fun isTaskbarHidden(@SystemUiStateFlags flags: Long): Boolean { + return ((hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_DEVICE_DREAMING) && + !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING)) || + (flags and QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK) != + QuickStepContract.WAKEFULNESS_AWAKE) + } + private fun hasAnyFlag(@SystemUiStateFlags flags: Long, flagMask: Long): Boolean { return (flags and flagMask) != 0L } diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java index 304b8f47e..d92cc86a6 100644 --- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java +++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java @@ -15,6 +15,7 @@ */ package com.android.quickstep.util; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.Display.DEFAULT_DISPLAY; import android.content.Context; @@ -26,22 +27,33 @@ import android.view.WindowManager; import android.view.WindowMetrics; import com.android.internal.policy.SystemBarUtils; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.util.WindowBounds; import com.android.launcher3.util.window.CachedDisplayInfo; import com.android.launcher3.util.window.WindowManagerProxy; -import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.SystemUiProxy; +import com.android.window.flags.Flags; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import java.util.List; import java.util.Set; +import javax.inject.Inject; + /** * Extension of {@link WindowManagerProxy} with some assumption for the default system Launcher */ +@LauncherAppSingleton public class SystemWindowManagerProxy extends WindowManagerProxy { - public SystemWindowManagerProxy(Context context) { + private final DesktopVisibilityController mDesktopVisibilityController; + + + @Inject + public SystemWindowManagerProxy(DesktopVisibilityController desktopVisibilityController) { super(true); + mDesktopVisibilityController = desktopVisibilityController; } @Override @@ -51,10 +63,55 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { } @Override - public boolean isInDesktopMode() { - DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - return desktopController != null && desktopController.areDesktopTasksVisible(); + public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) { + mDesktopVisibilityController.registerDesktopVisibilityListener(listener); + } + + @Override + public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) { + mDesktopVisibilityController.unregisterDesktopVisibilityListener(listener); + } + + @Override + public boolean isInDesktopMode(int displayId) { + return mDesktopVisibilityController.isInDesktopMode(displayId); + } + + @Override + public boolean showLockedTaskbarOnHome(Context displayInfoContext) { + if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) { + return false; + } + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) { + return false; + } + final boolean isFreeformDisplay = displayInfoContext.getResources().getConfiguration() + .windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM; + return isFreeformDisplay; + } + + @Override + public boolean showDesktopTaskbarForFreeformDisplay(Context displayInfoContext) { + if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) { + return false; + } + + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) { + return false; + } + + if (!Flags.enableDesktopTaskbarOnFreeformDisplays()) { + return false; + } + + final boolean isFreeformDisplay = displayInfoContext.getResources().getConfiguration() + .windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM; + return isFreeformDisplay; + } + + @Override + public boolean isHomeVisible(Context context) { + return SystemUiProxy.INSTANCE.get(context).getHomeVisibilityState().isHomeVisible(); } @Override diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java index 9a010429d..027dc083c 100644 --- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java +++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.util.Log; import androidx.annotation.Nullable; @@ -45,7 +46,7 @@ public class TISBindHelper implements ServiceConnection { // Max backoff caps at 5 mins private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; - private final Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Runnable mConnectionRunnable = this::internalBindToTIS; private final Context mContext; private final Consumer mConnectionCallback; diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java deleted file mode 100644 index 98d363ef5..000000000 --- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java +++ /dev/null @@ -1,136 +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.quickstep.util; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; - -import com.android.launcher3.util.IntArray; - -import java.lang.annotation.Retention; - -/** - * Helper class for navigating RecentsView grid tasks via arrow keys and tab. - */ -public class TaskGridNavHelper { - public static final int CLEAR_ALL_PLACEHOLDER_ID = -1; - public static final int INVALID_FOCUSED_TASK_ID = -1; - - public static final int DIRECTION_UP = 0; - public static final int DIRECTION_DOWN = 1; - public static final int DIRECTION_LEFT = 2; - public static final int DIRECTION_RIGHT = 3; - public static final int DIRECTION_TAB = 4; - - @Retention(SOURCE) - @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB}) - public @interface TASK_NAV_DIRECTION {} - - private final IntArray mOriginalTopRowIds; - private IntArray mTopRowIds; - private IntArray mBottomRowIds; - private final int mFocusedTaskId; - - public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) { - mFocusedTaskId = focusedTaskId; - mOriginalTopRowIds = topIds.clone(); - generateTaskViewIdGrid(topIds, bottomIds); - } - - private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray) { - boolean hasFocusedTask = mFocusedTaskId != INVALID_FOCUSED_TASK_ID; - int maxSize = - Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0); - int minSize = - Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0); - - // Add the focused task to the beginning of both arrays if it exists. - if (hasFocusedTask) { - topRowIdArray.add(0, mFocusedTaskId); - bottomRowIdArray.add(0, mFocusedTaskId); - } - - // Fill in the shorter array with the ids from the longer one. - for (int i = minSize; i < maxSize; i++) { - if (i >= topRowIdArray.size()) { - topRowIdArray.add(bottomRowIdArray.get(i)); - } else { - bottomRowIdArray.add(topRowIdArray.get(i)); - } - } - - // Add the clear all button to the end of both arrays - topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID); - bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID); - - mTopRowIds = topRowIdArray; - mBottomRowIds = bottomRowIdArray; - } - - /** - * Returns the id of the next page in the grid or -1 for the clear all button. - */ - public int getNextGridPage(int currentPageTaskViewId, int delta, - @TASK_NAV_DIRECTION int direction, boolean cycle) { - boolean inTop = mTopRowIds.contains(currentPageTaskViewId); - int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId) - : mBottomRowIds.indexOf(currentPageTaskViewId); - int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size()); - int nextIndex = index + delta; - - switch (direction) { - case DIRECTION_UP: - case DIRECTION_DOWN: { - return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index); - } - case DIRECTION_LEFT: { - int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1); - return inTop ? mTopRowIds.get(boundedIndex) - : mBottomRowIds.get(boundedIndex); - } - case DIRECTION_RIGHT: { - int boundedIndex = - cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max( - nextIndex, 0); - boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId); - return inOriginalTop ? mTopRowIds.get(boundedIndex) - : mBottomRowIds.get(boundedIndex); - } - case DIRECTION_TAB: { - int boundedIndex = - cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min( - nextIndex, maxSize - 1); - if (delta >= 0) { - return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index) - ? mBottomRowIds.get(index) - : mTopRowIds.get(boundedIndex); - } else { - if (mTopRowIds.contains(currentPageTaskViewId)) { - return mBottomRowIds.get(boundedIndex); - } else { - // Go up to top if there is task above - return mTopRowIds.get(index) != mBottomRowIds.get(index) - ? mTopRowIds.get(index) - : mBottomRowIds.get(boundedIndex); - } - } - } - default: - return currentPageTaskViewId; - } - } -} diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt new file mode 100644 index 000000000..0e788015c --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import com.android.launcher3.util.IntArray +import kotlin.math.abs +import kotlin.math.max + +/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */ +class TaskGridNavHelper( + private val topIds: IntArray, + bottomIds: IntArray, + largeTileIds: List, + hasAddDesktopButton: Boolean, +) { + private val topRowIds = mutableListOf() + private val bottomRowIds = mutableListOf() + + init { + // Add AddDesktopButton and lage tiles to both rows. + if (hasAddDesktopButton) { + topRowIds += ADD_DESK_PLACEHOLDER_ID + bottomRowIds += ADD_DESK_PLACEHOLDER_ID + } + topRowIds += largeTileIds + bottomRowIds += largeTileIds + + // Add row ids to their respective rows. + topRowIds += topIds + bottomRowIds += bottomIds + + // Fill in the shorter array with the ids from the longer one. + topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0)) + bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0)) + + // Add the clear all button to the end of both arrays. + topRowIds += CLEAR_ALL_PLACEHOLDER_ID + bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID + } + + /** Returns the id of the next page in the grid or -1 for the clear all button. */ + fun getNextGridPage( + currentPageTaskViewId: Int, + delta: Int, + direction: TaskNavDirection, + cycle: Boolean, + ): Int { + val inTop = topRowIds.contains(currentPageTaskViewId) + val index = + if (inTop) topRowIds.indexOf(currentPageTaskViewId) + else bottomRowIds.indexOf(currentPageTaskViewId) + val maxSize = max(topRowIds.size, bottomRowIds.size) + val nextIndex = index + delta + + return when (direction) { + TaskNavDirection.UP, + TaskNavDirection.DOWN -> { + if (inTop) bottomRowIds[index] else topRowIds[index] + } + TaskNavDirection.LEFT -> { + val boundedIndex = + if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1) + if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] + } + TaskNavDirection.RIGHT -> { + val boundedIndex = + if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex) + else nextIndex.coerceAtLeast(0) + val inOriginalTop = topIds.contains(currentPageTaskViewId) + if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] + } + TaskNavDirection.TAB -> { + val boundedIndex = + if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize) + else nextIndex.coerceAtMost(maxSize - 1) + if (delta >= 0) { + if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index] + else topRowIds[boundedIndex] + } else { + if (topRowIds.contains(currentPageTaskViewId)) { + if (boundedIndex < 0) { + // If no cycling, always return the first task. + topRowIds[0] + } else { + bottomRowIds[boundedIndex] + } + } else { + // Go up to top if there is task above + if (topRowIds[index] != bottomRowIds[index]) topRowIds[index] + else bottomRowIds[boundedIndex] + } + } + } + else -> currentPageTaskViewId + } + } + + /** + * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab + * navigation, starting from the initial TaskView ID, towards the start or end of the grid. + * + *

A positive delta moves forward in the tab order towards the end of the grid, while a + * negative value moves backward towards the beginning. The offset is the distance between + * columns the tasks are in. + */ + fun gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId: Int, + towardsStart: Boolean, + ): Sequence> = sequence { + val draggedTaskViewColumn = getColumn(initialTaskViewId) + var nextTaskViewId: Int = initialTaskViewId + var previousTaskViewId: Int = Int.MIN_VALUE + while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) { + previousTaskViewId = nextTaskViewId + nextTaskViewId = + getNextGridPage( + nextTaskViewId, + if (towardsStart) -1 else 1, + TaskNavDirection.TAB, + cycle = false, + ) + if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) { + val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn) + yield(Pair(nextTaskViewId, columnOffset)) + } + } + } + + /** Returns the column of a task's id in the grid. */ + private fun getColumn(taskViewId: Int): Int = + if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId) + else bottomRowIds.indexOf(taskViewId) + + enum class TaskNavDirection { + UP, + DOWN, + LEFT, + RIGHT, + TAB, + } + + companion object { + const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1 + const val ADD_DESK_PLACEHOLDER_ID: Int = -2 + } +} diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java index 69137cc1b..43ef39c25 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java @@ -17,6 +17,7 @@ package com.android.quickstep.util; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.systemui.shared.recents.model.Task; @@ -94,6 +95,7 @@ public class TaskKeyByLastActiveTimeCache implements TaskKeyCache { * Gets the entry if it is still valid */ @Override + @Nullable public synchronized V getAndInvalidateIfModified(Task.TaskKey key) { Entry entry = mMap.get(key.id); if (entry != null && entry.mKey.windowingMode == key.windowingMode diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java index 8ee78ab0b..9df0993a7 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java @@ -15,6 +15,8 @@ */ package com.android.quickstep.util; +import androidx.annotation.Nullable; + import com.android.systemui.shared.recents.model.Task; import java.util.function.Predicate; @@ -44,6 +46,7 @@ public interface TaskKeyCache { /** * Gets the entry if it is still valid. */ + @Nullable V getAndInvalidateIfModified(Task.TaskKey key); /** diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java index 89f5d41da..9fe8cc954 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java @@ -17,6 +17,8 @@ package com.android.quickstep.util; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.systemui.shared.recents.model.Task.TaskKey; import java.util.LinkedHashMap; @@ -59,6 +61,7 @@ public class TaskKeyLruCache implements TaskKeyCache { /** * Gets the entry if it is still valid */ + @Nullable public synchronized V getAndInvalidateIfModified(TaskKey key) { Entry entry = mMap.get(key.id); diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java index e80d2a6d3..40a328ccf 100644 --- a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java @@ -98,10 +98,7 @@ public class TaskRemovedDuringLaunchListener { final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback; RecentsModel.INSTANCE.get(mContext).isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> { if (taskRemoved) { - ActiveGestureLog.INSTANCE.addLog( - new ActiveGestureLog.CompoundString("Launch failed, task (id=") - .append(launchedTaskId) - .append(") finished mid transition")); + ActiveGestureProtoLogProxy.logTaskLaunchFailed(launchedTaskId); taskLaunchFailedCallback.run(); } }, (task) -> true /* filter */); diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java index 91e837699..6e2d469d1 100644 --- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java @@ -16,16 +16,11 @@ package com.android.quickstep.util; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; - -import android.app.Activity; import android.app.ActivityManager; import android.util.Log; import androidx.annotation.NonNull; -import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter; -import com.android.quickstep.RecentsModel; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 49f4e5f70..661fe898e 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -24,10 +24,8 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITIO import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation; import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation; -import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell; import android.animation.TimeInterpolator; import android.content.Context; @@ -43,7 +41,6 @@ import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.app.animation.Interpolators; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; @@ -52,9 +49,10 @@ import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.BaseActivityInterface; import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.DesktopFullscreenDrawParams; +import com.android.quickstep.FullscreenDrawParams; import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; -import com.android.quickstep.views.TaskView.FullscreenDrawParams; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; @@ -99,11 +97,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private final FullscreenDrawParams mCurrentFullscreenParams; public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat(); public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat(); + public final AnimatedFloat taskGridTranslationX = new AnimatedFloat(); + public final AnimatedFloat taskGridTranslationY = new AnimatedFloat(); // Carousel properties public final AnimatedFloat carouselScale = new AnimatedFloat(); - public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat(); - public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat(); // RecentsView properties public final AnimatedFloat recentsViewScale = new AnimatedFloat(); @@ -118,19 +116,28 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private SplitBounds mSplitBounds; private Boolean mDrawsBelowRecents = null; private boolean mIsGridTask; - private boolean mIsDesktopTask; - private boolean mScaleToCarouselTaskSize = false; + private final boolean mIsDesktopTask; + private boolean mIsAnimatingToCarousel = false; private int mTaskRectTranslationX; private int mTaskRectTranslationY; + private int mDesktopTaskIndex = 0; - public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) { + @Nullable + private Matrix mTaskRectTransform = null; + + public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy, + boolean isDesktop, int desktopTaskIndex) { mContext = context; mSizeStrategy = sizeStrategy; + mIsDesktopTask = isDesktop; + mDesktopTaskIndex = desktopTaskIndex; mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init", () -> new RecentsOrientedState(context, sizeStrategy, i -> { })); mOrientationState.setGestureActive(true); - mCurrentFullscreenParams = new FullscreenDrawParams(context); + mCurrentFullscreenParams = mIsDesktopTask + ? new DesktopFullscreenDrawParams(context) + : new FullscreenDrawParams(context); mOrientationStateId = mOrientationState.getStateId(); Resources resources = context.getResources(); mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources); @@ -144,6 +151,9 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mDp = dp; mLayoutValid = false; mOrientationState.setDeviceProfile(dp); + if (enableGridOnlyOverview()) { + mIsGridTask = dp.isTablet && !mIsDesktopTask; + } calculateTaskSize(); } @@ -155,14 +165,16 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { if (mIsGridTask) { mSizeStrategy.calculateGridTaskSize(mContext, mDp, mFullTaskSize, mOrientationState.getOrientationHandler()); + if (enableGridOnlyOverview()) { + mSizeStrategy.calculateTaskSize(mContext, mDp, mCarouselTaskSize, + mOrientationState.getOrientationHandler()); + } } else { mSizeStrategy.calculateTaskSize(mContext, mDp, mFullTaskSize, mOrientationState.getOrientationHandler()); - } - - if (enableGridOnlyOverview()) { - mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize, - mOrientationState.getOrientationHandler()); + if (enableGridOnlyOverview()) { + mCarouselTaskSize.set(mFullTaskSize); + } } if (mSplitBounds != null) { @@ -172,7 +184,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mTaskRect.set(mFullTaskSize); mOrientationState.getOrientationHandler() .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition); - mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); } else if (mIsDesktopTask) { // For desktop, tasks can take up only part of the screen size. // Full task size represents the whole screen size, but scaled down to fit in recents. @@ -186,10 +197,19 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mTaskRect.scale(scale); // Ensure the task rect is inside the full task rect mTaskRect.offset(mFullTaskSize.left, mFullTaskSize.top); + + Rect taskDimension = new Rect(0, 0, (int) fullscreenTaskDimension.x, + (int) fullscreenTaskDimension.y); + mTmpCropRect.set(mThumbnailPosition); + if (mTmpCropRect.setIntersect(taskDimension, mThumbnailPosition)) { + mTmpCropRect.offset(-mThumbnailPosition.left, -mThumbnailPosition.top); + } else { + mTmpCropRect.setEmpty(); + } } else { mTaskRect.set(mFullTaskSize); - mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); } + mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); } /** @@ -209,12 +229,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } // Copy mFullTaskSize instead of updating it directly so it could be reused next time // without recalculating - Rect scaleRect = new Rect(); - if (mScaleToCarouselTaskSize) { - scaleRect.set(mCarouselTaskSize); - } else { - scaleRect.set(mFullTaskSize); - } + Rect scaleRect = new Rect(mIsAnimatingToCarousel ? mCarouselTaskSize : mFullTaskSize); scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot); if (mPivotOverride != null) { @@ -247,8 +262,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } else { mStagePosition = runningTarget.taskId == splitInfo.leftTopTaskId ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; - mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds), - mStagePosition); } calculateTaskSize(); } @@ -283,13 +296,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mIsGridTask = isGridTask; } - /** - * Sets whether this task is part of desktop tasks in overview. - */ - public void setIsDesktopTask(boolean desktop) { - mIsDesktopTask = desktop; - } - /** * Apply translations on TaskRect's starting location. */ @@ -301,66 +307,24 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } /** - * Adds animation for all the components corresponding to transition from an app to overview. + * Override the pivot used to apply scale changes. */ - public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) { + public void setPivotOverride(PointF pivotOverride) { + mPivotOverride = pivotOverride; + getFullScreenScale(); + } + + /** + * Adds animation for all the components corresponding to transition from an app to carousel. + */ + public void addAppToCarouselAnim(PendingAnimation pa, Interpolator interpolator) { pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator); - float fullScreenScale; if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) { - // Move pivot to top right edge of the screen, to avoid task scaling down in opposite - // direction of app window movement, otherwise the animation will wiggle left and right. - // Also translate the app window to top right edge of the screen to simplify - // calculations. - taskPrimaryTranslation.value = mIsRecentsRtl - ? mDp.widthPx - mFullTaskSize.right - : -mFullTaskSize.left; - taskSecondaryTranslation.value = -mFullTaskSize.top; - mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0); - - // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale. - mScaleToCarouselTaskSize = true; + mIsAnimatingToCarousel = true; carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width(); - fullScreenScale = getFullScreenScale(); - - float carouselPrimaryTranslationTarget = mIsRecentsRtl - ? mCarouselTaskSize.right - mDp.widthPx - : mCarouselTaskSize.left; - float carouselSecondaryTranslationTarget = mCarouselTaskSize.top; - - // Expected carousel position's center is in the middle, and invariant of - // recentsViewScale. - float exceptedCarouselCenterX = mCarouselTaskSize.centerX(); - // Animating carousel translations linearly will result in a curved path, therefore - // we'll need to calculate the expected translation at each recentsView scale. Luckily - // primary and secondary follow the same translation, and primary is used here due to - // it being simpler. - Interpolator carouselTranslationInterpolator = t -> { - // recentsViewScale is calculated rather than using recentsViewScale.value, so that - // this interpolator works independently even if recentsViewScale don't animate. - float recentsViewScale = - Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR); - // Without the translation, the app window will animate from fullscreen into top - // right corner. - float expectedTaskCenterX = mIsRecentsRtl - ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f - : mCarouselTaskSize.width() * recentsViewScale / 2f; - // Calculate the expected translation, then work back the animatedFraction that - // results in this value. - float carouselPrimaryTranslation = - (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale; - return carouselPrimaryTranslation / carouselPrimaryTranslationTarget; - }; - - // Use addAnimatedFloat so this animation can later be canceled and animate to a - // different value in RecentsView.onPrepareGestureEndAnimation. - pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget, - carouselTranslationInterpolator); - pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget, - carouselTranslationInterpolator); - } else { - fullScreenScale = getFullScreenScale(); } - pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator); + pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, + interpolator); } /** @@ -404,6 +368,38 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return mMatrix; } + /** + * Sets a matrix used to transform the position of tasks. If set, this matrix is applied to + * the task rect after the task has been scaled and positioned inside the fulltask, but + * before scaling and translation of the whole recents view is performed. + */ + public void setTaskRectTransform(@Nullable Matrix taskRectTransform) { + mTaskRectTransform = taskRectTransform; + } + + /** + * Calculates the crop rect for desktop tasks given the current matrix. + */ + private void calculateDesktopTaskCropRect() { + // The approach here is to map a rect that represents the untransformed thumbnail position + // using the current matrix. This will give us a rect that can be intersected with + // [mFullTaskSize]. Using the intersection, we then compute how much of the task window that + // needs to be cropped (which will be nothing if the window is entirely within the desktop). + mTempRectF.set(0, 0, mThumbnailPosition.width(), mThumbnailPosition.height()); + mMatrix.mapRect(mTempRectF); + + float offsetX = mTempRectF.left; + float offsetY = mTempRectF.top; + float scale = mThumbnailPosition.width() / mTempRectF.width(); + + if (mTempRectF.intersect(mFullTaskSize.left, mFullTaskSize.top, mFullTaskSize.right, + mFullTaskSize.bottom)) { + mTempRectF.offset(-offsetX, -offsetY); + mTempRectF.scale(scale); + mTempRectF.round(mTmpCropRect); + } + } + /** * Applies the rotation on the matrix to so that it maps from launcher coordinate space to * window coordinate space. @@ -465,18 +461,29 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mMatrix.set(mPositionHelper.getMatrix()); - // Apply TaskView matrix: taskRect, translate + // Apply TaskView matrix: taskRect, optional transform, translate mMatrix.postTranslate(mTaskRect.left, mTaskRect.top); + if (mTaskRectTransform != null) { + mMatrix.postConcat(mTaskRectTransform); + + // Calculate cropping for desktop tasks. The order is important since it uses the + // current matrix. Therefore we calculate it here, after applying the task rect + // transform, but before applying scaling/translation that affects the whole + // recentsview. + if (mIsDesktopTask) { + calculateDesktopTaskCropRect(); + } + } + mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE, taskPrimaryTranslation.value); mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, taskSecondaryTranslation.value); + mMatrix.postTranslate(taskGridTranslationX.value, taskGridTranslationY.value); - mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y); - mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE, - carouselPrimaryTranslation.value); - mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, - carouselSecondaryTranslation.value); + mMatrix.postScale(carouselScale.value, carouselScale.value, + mIsRecentsRtl ? mCarouselTaskSize.right : mCarouselTaskSize.left, + mCarouselTaskSize.top); mOrientationState.getOrientationHandler().setPrimary( mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value); @@ -489,10 +496,12 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { recentsViewPrimaryTranslation.value); applyWindowToHomeRotation(mMatrix); - // Crop rect is the inverse of thumbnail matrix - mTempRectF.set(0, 0, taskWidth, taskHeight); - mInversePositionMatrix.mapRect(mTempRectF); - mTempRectF.roundOut(mTmpCropRect); + if (!mIsDesktopTask) { + // Crop rect is the inverse of thumbnail matrix + mTempRectF.set(0, 0, taskWidth, taskHeight); + mInversePositionMatrix.mapRect(mTempRectF); + mTempRectF.roundOut(mTmpCropRect); + } params.setProgress(1f - fullScreenProgress); params.applySurfaceParams(surfaceTransaction == null @@ -510,8 +519,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { + " taskRect: " + mTaskRect + " taskPrimaryT: " + taskPrimaryTranslation.value + " taskSecondaryT: " + taskSecondaryTranslation.value - + " carouselPrimaryT: " + carouselPrimaryTranslation.value - + " carouselSecondaryT: " + carouselSecondaryTranslation.value + + " taskGridTranslationX: " + taskGridTranslationX.value + + " taskGridTranslationY: " + taskGridTranslationY.value + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value + " recentsScroll: " + recentsViewScroll.value @@ -528,21 +537,12 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { // If mDrawsBelowRecents is unset, no reordering will be enforced. if (mDrawsBelowRecents != null) { - // In legacy transitions, the animation leashes remain in same hierarchy in the - // TaskDisplayArea, so we don't want to bump the layer too high otherwise it will - // conflict with layers that WM core positions (ie. the input consumers). For shell - // transitions, the animation leashes are reparented to an animation container so we - // can bump layers as needed. - if (ENABLE_SHELL_TRANSITIONS) { - builder.setLayer(mDrawsBelowRecents - ? Integer.MIN_VALUE + app.prefixOrderIndex - // 1000 is an arbitrary number to give room for multiple layers. - : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex); - } else { - builder.setLayer(mDrawsBelowRecents - ? Integer.MIN_VALUE + app.prefixOrderIndex - : 0); - } + // In shell transitions, the animation leashes are reparented to an animation container + // so we can bump layers as needed. + builder.setLayer(mDrawsBelowRecents + // 1000 is an arbitrary number to give room for multiple layers. + ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex + : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex); } } @@ -551,7 +551,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { * TaskView */ public float getCurrentCornerRadius() { - float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius(); + float visibleRadius = mCurrentFullscreenParams.getCurrentCornerRadius(); mTempPoint[0] = visibleRadius; mTempPoint[1] = 0; mInversePositionMatrix.mapVectors(mTempPoint); diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java index 66bff730b..519ef60ec 100644 --- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import android.annotation.NonNull; import android.os.UserHandle; import com.android.systemui.shared.recents.model.Task; @@ -36,7 +37,7 @@ public interface TaskVisualsChangeListener { /** * Called when the icon for a task changes */ - default void onTaskIconChanged(String pkg, UserHandle user) {} + default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {} /** * Called when the icon for a task changes diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java index ebcef30b9..716803a80 100644 --- a/quickstep/src/com/android/quickstep/util/TransformParams.java +++ b/quickstep/src/com/android/quickstep/util/TransformParams.java @@ -19,9 +19,16 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import android.util.FloatProperty; import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.annotation.VisibleForTesting; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; +import com.android.window.flags.Flags; + +import java.util.function.Supplier; public class TransformParams { @@ -56,15 +63,24 @@ public class TransformParams { private float mTargetAlpha; private float mCornerRadius; private RemoteAnimationTargets mTargetSet; + private TransitionInfo mTransitionInfo; + private boolean mCornerRadiusIsOverridden; private SurfaceTransactionApplier mSyncTransactionApplier; + private Supplier mSurfaceTransactionSupplier; private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE; private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE; public TransformParams() { + this(SurfaceTransaction::new); + } + + @VisibleForTesting + public TransformParams(Supplier surfaceTransactionSupplier) { mProgress = 0; mTargetAlpha = 1; mCornerRadius = -1; + mSurfaceTransactionSupplier = surfaceTransactionSupplier; } /** @@ -106,6 +122,15 @@ public class TransformParams { return this; } + /** + * Provides the {@code TransitionInfo} of the transition that this transformation stems from. + */ + public TransformParams setTransitionInfo(TransitionInfo transitionInfo) { + mTransitionInfo = transitionInfo; + mCornerRadiusIsOverridden = false; + return this; + } + /** * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that * are computed based on these TransformParams. @@ -136,26 +161,31 @@ public class TransformParams { /** Builds the SurfaceTransaction from the given BuilderProxy params. */ public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) { RemoteAnimationTargets targets = mTargetSet; - SurfaceTransaction transaction = new SurfaceTransaction(); + SurfaceTransaction transaction = mSurfaceTransactionSupplier.get(); if (targets == null) { return transaction; } for (int i = 0; i < targets.unfilteredApps.length; i++) { RemoteAnimationTarget app = targets.unfilteredApps[i]; SurfaceProperties builder = transaction.forSurface(app.leash); + BuilderProxy targetProxy = + app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME + ? mHomeBuilderProxy + : (app.mode == targets.targetMode ? proxy : mBaseBuilderProxy); if (app.mode == targets.targetMode) { - int activityType = app.windowConfiguration.getActivityType(); - if (activityType == ACTIVITY_TYPE_HOME) { - mHomeBuilderProxy.onBuildTargetParams(builder, app, this); - } else { - builder.setAlpha(getTargetAlpha()); - proxy.onBuildTargetParams(builder, app, this); - } - } else { - mBaseBuilderProxy.onBuildTargetParams(builder, app, this); + builder.setAlpha(getTargetAlpha()); + } + targetProxy.onBuildTargetParams(builder, app, this); + // Override the corner radius for {@code app} with the leash used by Shell, so that it + // doesn't interfere with the window clip and corner radius applied here. + // Only override the corner radius once - so that we don't accidentally override at the + // end of transition after WM Shell has reset the corner radius of the task. + if (!mCornerRadiusIsOverridden) { + overrideFreeformChangeLeashCornerRadiusToZero(app, transaction.getTransaction()); } } + mCornerRadiusIsOverridden = true; // always put wallpaper layer to bottom. final int wallpaperLength = targets.wallpapers != null ? targets.wallpapers.length : 0; @@ -166,7 +196,33 @@ public class TransformParams { return transaction; } - // Pubic getters so outside packages can read the values. + private void overrideFreeformChangeLeashCornerRadiusToZero( + RemoteAnimationTarget app, SurfaceControl.Transaction transaction) { + if (!Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + return; + } + if (app.taskInfo == null || !app.taskInfo.isFreeform()) { + return; + } + + SurfaceControl changeLeash = getChangeLeashForApp(app); + if (changeLeash != null) { + transaction.setCornerRadius(changeLeash, 0); + } + } + + private SurfaceControl getChangeLeashForApp(RemoteAnimationTarget app) { + if (mTransitionInfo == null) return null; + for (TransitionInfo.Change change : mTransitionInfo.getChanges()) { + if (change.getTaskInfo() == null) continue; + if (change.getTaskInfo().taskId == app.taskId) { + return change.getLeash(); + } + } + return null; + } + + // Public getters so outside packages can read the values. public float getProgress() { return mProgress; diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java index 0a97793b2..32e0e132e 100644 --- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java +++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java @@ -30,6 +30,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.util.FloatProperty; +import android.util.Log; import android.view.View; import com.android.app.animation.Interpolators; @@ -51,6 +52,8 @@ import com.android.systemui.plugins.ResourceProvider; */ public class WorkspaceRevealAnim { + private static final String TAG = "WorkspaceRevealAnim"; + // Should be used for animations running alongside this WorkspaceRevealAnim. public static final int DURATION_MS = 350; private static final FloatProperty> WORKSPACE_SCALE_PROPERTY = @@ -97,6 +100,19 @@ public class WorkspaceRevealAnim { mAnimators.setDuration(DURATION_MS); mAnimators.setInterpolator(Interpolators.DECELERATED_EASE); + mAnimators.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + Log.d(TAG, "onAnimationCancel"); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + Log.d(TAG, "onAnimationEnd: workspace alpha = " + workspace.getAlpha()); + } + }); } private void addRevealAnimatorsForView(T v, FloatProperty scaleProperty) { diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt index 09563f552..915c9e530 100644 --- a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt +++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt @@ -22,7 +22,6 @@ import com.android.launcher3.Alarm import com.android.launcher3.DeviceProfile import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener import com.android.launcher3.anim.PendingAnimation -import com.android.launcher3.config.FeatureFlags import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -30,7 +29,7 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr /** Controls animations that are happening during unfolding foldable devices */ class LauncherUnfoldTransitionController( private val launcher: QuickstepLauncher, - private val progressProvider: ProxyUnfoldTransitionProvider + private val progressProvider: ProxyUnfoldTransitionProvider, ) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener { private var isTablet: Boolean? = null @@ -57,10 +56,6 @@ class LauncherUnfoldTransitionController( } override fun onDeviceProfileChanged(dp: DeviceProfile) { - if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - return - } - if (isTablet != null && dp.isTablet != isTablet) { // We should preemptively start the animation only if: // - We changed to the unfolded screen @@ -93,7 +88,7 @@ class LauncherUnfoldTransitionController( provider = this, factory = this::onPrepareUnfoldAnimation, duration = - 1000L // The expected duration for the animation. Then only comes to play if we have + 1000L, // The expected duration for the animation. Then only comes to play if we have // to run the animation ourselves in case sysui misses the end signal ) timeoutAlarm.cancelAlarm() @@ -119,7 +114,7 @@ class LauncherUnfoldTransitionController( launcher, isVertical, dp.displayInfo.currentSize, - anim + anim, ) } diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt new file mode 100644 index 000000000..37359a1b3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt @@ -0,0 +1,114 @@ +/* + * 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.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.util.AttributeSet +import android.util.FloatProperty +import android.widget.ImageButton +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.R +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator + +/** + * Button for supporting multiple desktop sessions. The button will be next to the first TaskView + * inside overview, while clicking this button will create a new desktop session. + */ +class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ImageButton(context, attrs) { + + private val addDeskButtonAlpha = MultiValueAlpha(this, Alpha.entries.size) + var contentAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.CONTENT) + var visibilityAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.VISIBILITY) + + private val multiTranslationX = + MultiPropertyFactory(this, VIEW_TRANSLATE_X, TranslationX.entries.size) { a: Float, b: Float + -> + a + b + } + var gridTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.GRID) + var offsetTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.OFFSET) + + private val focusBorderAnimator: BorderAnimator = + createSimpleBorderAnimator( + context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_size), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getBorderBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.AddDesktopButton) + .getColor( + R.styleable.AddDesktopButton_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + + var borderEnabled = false + set(value) { + if (field == value) { + return + } + field = value + focusBorderAnimator.setBorderVisibility(visible = field && isFocused, animated = true) + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (borderEnabled) { + focusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true) + } + } + + private fun getBorderBounds(bounds: Rect) { + bounds.set(0, 0, width, height) + val outlinePadding = + context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_outline_padding) + bounds.inset(-outlinePadding, -outlinePadding) + } + + override fun draw(canvas: Canvas) { + focusBorderAnimator.drawBorder(canvas) + super.draw(canvas) + } + + companion object { + private enum class Alpha { + CONTENT, + VISIBILITY, + } + + private enum class TranslationX { + GRID, + OFFSET, + } + + @JvmField + val VISIBILITY_ALPHA: FloatProperty = + KFloatProperty(AddDesktopButton::visibilityAlpha) + } +} diff --git a/quickstep/src/com/android/quickstep/views/BlurUtils.kt b/quickstep/src/com/android/quickstep/views/BlurUtils.kt new file mode 100644 index 000000000..d6b2a055b --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/BlurUtils.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle + +/** Applies blur either behind launcher surface or live tile app. */ +class BlurUtils(private val recentsView: RecentsView<*, *>) { + + fun setDrawLiveTileBelowRecents(drawBelowRecents: Boolean) { + val liveTileRemoteTargetHandles = + if ( + recentsView.remoteTargetHandles != null && + recentsView.recentsAnimationController != null + ) + recentsView.remoteTargetHandles + else null + setDrawBelowRecents(drawBelowRecents, liveTileRemoteTargetHandles) + } + + /** + * Set surface in [remoteTargetHandles] to be above or below Recents layer, and update the base + * layer to apply blur to in BaseDepthController. + */ + fun setDrawBelowRecents( + drawBelowRecents: Boolean, + remoteTargetHandles: Array? = null, + ) { + remoteTargetHandles?.forEach { it.taskViewSimulator.setDrawsBelowRecents(drawBelowRecents) } + if (enableOverviewBackgroundWallpaperBlur()) { + recentsView.depthController?.setBaseSurfaceOverride( + // Blurs behind launcher layer. + if (!drawBelowRecents || remoteTargetHandles == null) { + null + } else { + // Blurs behind live tile. blur will be applied behind window + // which farthest from user in case of desktop and split apps. + remoteTargetHandles + .maxByOrNull { it.transformParams.targetSet.firstAppTarget.leash.layerId } + ?.transformParams + ?.targetSet + ?.firstAppTarget + ?.leash + } + ) + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java deleted file mode 100644 index c3efc3cbc..000000000 --- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java +++ /dev/null @@ -1,344 +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.quickstep.views; - -import static com.android.launcher3.Flags.enableGridOnlyOverview; -import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.FloatProperty; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Flags; -import com.android.launcher3.R; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.BorderAnimator; - -import kotlin.Unit; - -public class ClearAllButton extends Button { - - public static final FloatProperty VISIBILITY_ALPHA = - new FloatProperty("visibilityAlpha") { - @Override - public Float get(ClearAllButton view) { - return view.mVisibilityAlpha; - } - - @Override - public void setValue(ClearAllButton view, float v) { - view.setVisibilityAlpha(v); - } - }; - - public static final FloatProperty DISMISS_ALPHA = - new FloatProperty("dismissAlpha") { - @Override - public Float get(ClearAllButton view) { - return view.mDismissAlpha; - } - - @Override - public void setValue(ClearAllButton view, float v) { - view.setDismissAlpha(v); - } - }; - - private final RecentsViewContainer mContainer; - private float mScrollAlpha = 1; - private float mContentAlpha = 1; - private float mVisibilityAlpha = 1; - private float mDismissAlpha = 1; - private float mFullscreenProgress = 1; - private float mGridProgress = 1; - - private boolean mIsRtl; - private float mNormalTranslationPrimary; - private float mFullscreenTranslationPrimary; - private float mGridTranslationPrimary; - private float mGridScrollOffset; - private float mScrollOffsetPrimary; - - private int mSidePadding; - private int mOutlinePadding; - private boolean mBorderEnabled; - @Nullable - private final BorderAnimator mFocusBorderAnimator; - - public ClearAllButton(Context context, AttributeSet attrs) { - super(context, attrs); - mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; - mContainer = RecentsViewContainer.containerFromContext(context); - - if (Flags.enableFocusOutline()) { - TypedArray styledAttrs = context.obtainStyledAttributes(attrs, - R.styleable.ClearAllButton); - Resources resources = getResources(); - mOutlinePadding = resources.getDimensionPixelSize( - R.dimen.recents_clear_all_outline_padding); - mFocusBorderAnimator = - BorderAnimator.createSimpleBorderAnimator( - /* borderRadiusPx= */ resources.getDimensionPixelSize( - R.dimen.recents_clear_all_outline_radius), - /* borderWidthPx= */ context.getResources().getDimensionPixelSize( - R.dimen.keyboard_quick_switch_border_width), - /* boundsBuilder= */ this::updateBorderBounds, - /* targetView= */ this, - /* borderColor= */ styledAttrs.getColor( - R.styleable.ClearAllButton_focusBorderColor, - DEFAULT_BORDER_COLOR)); - styledAttrs.recycle(); - } else { - mFocusBorderAnimator = null; - } - } - - private Unit updateBorderBounds(@NonNull Rect bounds) { - bounds.set(0, 0, getWidth(), getHeight()); - // Make the value negative to form a padding between button and outline - bounds.inset(-mOutlinePadding, -mOutlinePadding); - return Unit.INSTANCE; - } - - @Override - public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (mFocusBorderAnimator != null && mBorderEnabled) { - mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true); - } - } - - /** - * Enable or disable showing border on focus change - */ - public void setBorderEnabled(boolean enabled) { - if (mBorderEnabled == enabled) { - return; - } - - mBorderEnabled = enabled; - if (mFocusBorderAnimator != null) { - mFocusBorderAnimator.setBorderVisibility(/* visible= */ - enabled && isFocused(), /* animated= */true); - } - } - - @Override - public void draw(Canvas canvas) { - if (mFocusBorderAnimator != null) { - mFocusBorderAnimator.drawBorder(canvas); - } - super.draw(canvas); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - RecentsPagedOrientationHandler orientationHandler = - getRecentsView().getPagedOrientationHandler(); - mSidePadding = orientationHandler.getClearAllSidePadding(getRecentsView(), mIsRtl); - } - - private RecentsView getRecentsView() { - return (RecentsView) getParent(); - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - super.onRtlPropertiesChanged(layoutDirection); - mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - public float getScrollAlpha() { - return mScrollAlpha; - } - - public void setContentAlpha(float alpha) { - if (mContentAlpha != alpha) { - mContentAlpha = alpha; - updateAlpha(); - } - } - - public void setVisibilityAlpha(float alpha) { - if (mVisibilityAlpha != alpha) { - mVisibilityAlpha = alpha; - updateAlpha(); - } - } - - public void setDismissAlpha(float alpha) { - if (mDismissAlpha != alpha) { - mDismissAlpha = alpha; - updateAlpha(); - } - } - - public void onRecentsViewScroll(int scroll, boolean gridEnabled) { - RecentsView recentsView = getRecentsView(); - if (recentsView == null) { - return; - } - - RecentsPagedOrientationHandler orientationHandler = - recentsView.getPagedOrientationHandler(); - float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight()); - if (orientationSize == 0) { - return; - } - - int clearAllScroll = recentsView.getClearAllScroll(); - int adjustedScrollFromEdge = Math.abs(scroll - clearAllScroll); - float shift = Math.min(adjustedScrollFromEdge, orientationSize); - mNormalTranslationPrimary = mIsRtl ? -shift : shift; - if (!gridEnabled) { - mNormalTranslationPrimary += mSidePadding; - } - applyPrimaryTranslation(); - applySecondaryTranslation(); - float clearAllSpacing = - recentsView.getPageSpacing() + recentsView.getClearAllExtraPageSpacing(); - clearAllSpacing = mIsRtl ? -clearAllSpacing : clearAllSpacing; - mScrollAlpha = Math.max((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing, 0); - updateAlpha(); - } - - private void updateAlpha() { - final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha * mDismissAlpha; - setAlpha(alpha); - setClickable(Math.min(alpha, 1) == 1); - } - - public void setFullscreenTranslationPrimary(float fullscreenTranslationPrimary) { - mFullscreenTranslationPrimary = fullscreenTranslationPrimary; - applyPrimaryTranslation(); - } - - public void setGridTranslationPrimary(float gridTranslationPrimary) { - mGridTranslationPrimary = gridTranslationPrimary; - applyPrimaryTranslation(); - } - - public void setGridScrollOffset(float gridScrollOffset) { - mGridScrollOffset = gridScrollOffset; - } - - public void setScrollOffsetPrimary(float scrollOffsetPrimary) { - mScrollOffsetPrimary = scrollOffsetPrimary; - } - - public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) { - float scrollAdjustment = 0; - if (fullscreenEnabled) { - scrollAdjustment += mFullscreenTranslationPrimary; - } - if (gridEnabled) { - scrollAdjustment += mGridTranslationPrimary + mGridScrollOffset; - } - scrollAdjustment += mScrollOffsetPrimary; - return scrollAdjustment; - } - - public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) { - return getScrollAdjustment(fullscreenEnabled, gridEnabled); - } - - /** - * Adjust translation when this TaskView is about to be shown fullscreen. - * - * @param progress: 0 = no translation; 1 = translate according to TaskVIew translations. - */ - public void setFullscreenProgress(float progress) { - mFullscreenProgress = progress; - applyPrimaryTranslation(); - } - - /** - * Moves ClearAllButton between carousel and 2 row grid. - * - * @param gridProgress 0 = carousel; 1 = 2 row grid. - */ - public void setGridProgress(float gridProgress) { - mGridProgress = gridProgress; - applyPrimaryTranslation(); - } - - private void applyPrimaryTranslation() { - RecentsView recentsView = getRecentsView(); - if (recentsView == null) { - return; - } - - RecentsPagedOrientationHandler orientationHandler = - recentsView.getPagedOrientationHandler(); - orientationHandler.getPrimaryViewTranslate().set(this, - orientationHandler.getPrimaryValue(0f, getOriginalTranslationY()) - + mNormalTranslationPrimary + getFullscreenTrans( - mFullscreenTranslationPrimary) + getGridTrans(mGridTranslationPrimary)); - } - - private void applySecondaryTranslation() { - RecentsView recentsView = getRecentsView(); - if (recentsView == null) { - return; - } - - RecentsPagedOrientationHandler orientationHandler = - recentsView.getPagedOrientationHandler(); - orientationHandler.getSecondaryViewTranslate().set(this, - orientationHandler.getSecondaryValue(0f, getOriginalTranslationY())); - } - - private float getFullscreenTrans(float endTranslation) { - return mFullscreenProgress > 0 ? endTranslation : 0; - } - - private float getGridTrans(float endTranslation) { - return mGridProgress > 0 ? endTranslation : 0; - } - - /** - * Get the Y translation that is set in the original layout position, before scrolling. - */ - private float getOriginalTranslationY() { - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - if (deviceProfile.isTablet) { - if (enableGridOnlyOverview()) { - return (getRecentsView().getLastComputedTaskSize().height() - + deviceProfile.overviewTaskThumbnailTopMarginPx) / 2.0f - + deviceProfile.overviewRowSpacing; - } else { - return deviceProfile.overviewRowSpacing; - } - } - return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f; - } -} diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.kt b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt new file mode 100644 index 000000000..69c85eeae --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.util.AttributeSet +import android.util.FloatProperty +import android.widget.Button +import com.android.launcher3.Flags.enableFocusOutline +import com.android.launcher3.R +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import kotlin.math.abs +import kotlin.math.min + +class ClearAllButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + Button(context, attrs) { + + private val clearAllButtonAlpha = + object : MultiValueAlpha(this, Alpha.entries.size) { + override fun apply(value: Float) { + super.apply(value) + isClickable = value >= 1f + } + } + var scrollAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.SCROLL) + var contentAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.CONTENT) + var visibilityAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.VISIBILITY) + var dismissAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.DISMISS) + + var fullscreenProgress = 1f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + /** + * Moves ClearAllButton between carousel and 2 row grid. + * + * 0 = carousel; 1 = 2 row grid. + */ + var gridProgress = 1f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + private var normalTranslationPrimary = 0f + var fullscreenTranslationPrimary = 0f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + var gridTranslationPrimary = 0f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + /** Used to put the button at the middle in the secondary coordinate. */ + var taskAlignmentTranslationY = 0f + set(value) { + if (field == value) { + return + } + field = value + applySecondaryTranslation() + } + + var gridScrollOffset = 0f + var scrollOffsetPrimary = 0f + + private var sidePadding = 0 + var borderEnabled = false + set(value) { + if (field == value) { + return + } + field = value + focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) + } + + private val focusBorderAnimator: BorderAnimator? = + if (enableFocusOutline()) + createSimpleBorderAnimator( + context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_radius), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getBorderBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.ClearAllButton) + .getColor( + R.styleable.ClearAllButton_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + else null + + private fun getBorderBounds(bounds: Rect) { + bounds.set(0, 0, width, height) + val outlinePadding = + context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_padding) + // Make the value negative to form a padding between button and outline + bounds.inset(-outlinePadding, -outlinePadding) + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (borderEnabled) { + focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true) + } + } + + override fun draw(canvas: Canvas) { + focusBorderAnimator?.drawBorder(canvas) + super.draw(canvas) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + sidePadding = + recentsView?.let { it.pagedOrientationHandler?.getClearAllSidePadding(it, isLayoutRtl) } + ?: 0 + } + + private val recentsView: RecentsView<*, *>? + get() = parent as? RecentsView<*, *>? + + override fun hasOverlappingRendering() = false + + fun onRecentsViewScroll(scroll: Int, gridEnabled: Boolean) { + val recentsView = recentsView ?: return + + val orientationSize = + recentsView.pagedOrientationHandler.getPrimaryValue(width, height).toFloat() + if (orientationSize == 0f) { + return + } + + val clearAllScroll = recentsView.clearAllScroll + val adjustedScrollFromEdge = abs((scroll - clearAllScroll)).toFloat() + val shift = min(adjustedScrollFromEdge, orientationSize) + normalTranslationPrimary = if (isLayoutRtl) -shift else shift + if (!gridEnabled) { + normalTranslationPrimary += sidePadding.toFloat() + } + applyPrimaryTranslation() + applySecondaryTranslation() + var clearAllSpacing = recentsView.pageSpacing + recentsView.clearAllExtraPageSpacing + clearAllSpacing = if (isLayoutRtl) -clearAllSpacing else clearAllSpacing + scrollAlpha = + ((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing.toFloat()).coerceAtLeast( + 0f + ) + } + + fun getScrollAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean): Float { + var scrollAdjustment = 0f + if (fullscreenEnabled) { + scrollAdjustment += fullscreenTranslationPrimary + } + if (gridEnabled) { + scrollAdjustment += gridTranslationPrimary + gridScrollOffset + } + scrollAdjustment += scrollOffsetPrimary + return scrollAdjustment + } + + fun getOffsetAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean) = + getScrollAdjustment(fullscreenEnabled, gridEnabled) + + private fun applyPrimaryTranslation() { + val recentsView = recentsView ?: return + val orientationHandler = recentsView.pagedOrientationHandler + orientationHandler.primaryViewTranslate.set( + this, + (orientationHandler.getPrimaryValue(0f, taskAlignmentTranslationY) + + normalTranslationPrimary + + getFullscreenTrans(fullscreenTranslationPrimary) + + getGridTrans(gridTranslationPrimary)), + ) + } + + private fun applySecondaryTranslation() { + val recentsView = recentsView ?: return + val orientationHandler = recentsView.pagedOrientationHandler + orientationHandler.secondaryViewTranslate.set( + this, + orientationHandler.getSecondaryValue(0f, taskAlignmentTranslationY), + ) + } + + private fun getFullscreenTrans(endTranslation: Float) = + if (fullscreenProgress > 0) endTranslation else 0f + + private fun getGridTrans(endTranslation: Float) = if (gridProgress > 0) endTranslation else 0f + + companion object { + private enum class Alpha { + SCROLL, + CONTENT, + VISIBILITY, + DISMISS, + } + + @JvmField + val VISIBILITY_ALPHA: FloatProperty = + KFloatProperty(ClearAllButton::visibilityAlpha) + + @JvmField + val DISMISS_ALPHA: FloatProperty = + KFloatProperty(ClearAllButton::dismissAlpha) + } +} diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt new file mode 100644 index 000000000..ef044f4fd --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.FrameLayout + +class DesktopTaskContentView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + var cornerRadius: Float = 0f + set(value) { + field = value + invalidateOutline() + } + + private val bounds = Rect() + + init { + clipToOutline = true + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(bounds, cornerRadius) + } + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + bounds.set(0, 0, w, h) + invalidateOutline() + } +} diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 936f6a1c0..8b124555f 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -15,268 +15,514 @@ */ package com.android.quickstep.views +import android.annotation.SuppressLint import android.content.Context -import android.graphics.Point +import android.graphics.Matrix import android.graphics.PointF import android.graphics.Rect -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.shapes.RoundRectShape +import android.graphics.Rect.intersects +import android.graphics.RectF import android.util.AttributeSet import android.util.Log +import android.util.Size +import android.view.Display.INVALID_DISPLAY +import android.view.Gravity import android.view.View -import android.view.ViewGroup +import android.view.ViewStub +import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams +import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.testing.TestLogging +import com.android.launcher3.testing.shared.TestProtocol import com.android.launcher3.util.RunnableList import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.rects.lerpRect import com.android.launcher3.util.rects.set import com.android.quickstep.BaseContainerInterface +import com.android.quickstep.DesktopFullscreenDrawParams +import com.android.quickstep.FullscreenDrawParams +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.ViewUtils +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData +import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.util.DesktopTask import com.android.quickstep.util.RecentsOrientedState -import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops +import kotlin.math.roundToInt /** TaskView that contains all tasks that are part of the desktop. */ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - TaskView(context, attrs) { + TaskView( + context, + attrs, + type = TaskViewType.DESKTOP, + thumbnailFullscreenParams = DesktopFullscreenDrawParams(context), + ) { + val deskId + get() = desktopTask?.deskId ?: DesktopVisibilityController.INACTIVE_DESK_ID + + private var desktopTask: DesktopTask? = null + + private val contentViewFullscreenParams = FullscreenDrawParams(context) + + private val taskThumbnailViewDeprecatedPool = + if (!enableRefactorTaskThumbnail()) { + ViewPool( + context, + this, + R.layout.task_thumbnail_deprecated, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null - private val snapshotDrawParams = - object : FullscreenDrawParams(context) { - // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress. - override fun computeTaskCornerRadius(context: Context) = - computeWindowCornerRadius(context) - } private val taskThumbnailViewPool = - ViewPool( - context, - this, - R.layout.task_thumbnail, - VIEW_POOL_MAX_SIZE, - VIEW_POOL_INITIAL_SIZE - ) + if (enableRefactorTaskThumbnail()) { + ViewPool( + context, + this, + R.layout.task_thumbnail, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null + private val tempPointF = PointF() - private val tempRect = Rect() - private lateinit var backgroundView: View + private val lastComputedTaskSize = Rect() private lateinit var iconView: TaskViewIcon - private var childCountAtInflation = 0 + private lateinit var contentView: DesktopTaskContentView + private lateinit var backgroundView: View + + private var viewModel: DesktopTaskViewModel? = null + + /** + * Holds the default (user placed) positions of task windows. This can be moved into the + * viewModel once RefactorTaskThumbnail has been launched. + */ + private var fullscreenTaskPositions: List = emptyList() + + /** + * When enableDesktopExplodedView is enabled, this controls the gradual transition from the + * default positions to the organized non-overlapping positions. + */ + var explodeProgress = 0.0f + set(value) { + field = value + positionTaskWindows() + } + + var remoteTargetHandles: Array? = null + set(value) { + field = value + positionTaskWindows() + } + + override val displayId: Int + get() = + if (enableMultipleDesktops(context)) { + desktopTask?.displayId ?: INVALID_DISPLAY + } else { + super.displayId + } + + private fun getRemoteTargetHandle(taskId: Int): RemoteTargetHandle? = + remoteTargetHandles?.firstOrNull { + it.transformParams.targetSet.firstAppTargetTaskId == taskId + } override fun onFinishInflate() { super.onFinishInflate() - backgroundView = - findViewById(R.id.background)!!.apply { + iconView = + (findViewById(R.id.icon) as TaskViewIcon).apply { + setIcon( + this, + ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_desktop_with_bg, + context.theme, + ), + ) + setText(resources.getText(R.string.recent_task_desktop)) + } + contentView = + findViewById(R.id.desktop_content).apply { updateLayoutParams { topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx } - background = - ShapeDrawable(RoundRectShape(FloatArray(8) { taskCornerRadius }, null, null)) - .apply { - setTint( - resources.getColor( - android.R.color.system_neutral2_300, - context.theme - ) - ) - } + cornerRadius = contentViewFullscreenParams.currentCornerRadius + backgroundView = findViewById(R.id.background) + backgroundView.setBackgroundColor( + resources.getColor(android.R.color.system_neutral2_300, context.theme) + ) } - iconView = - getOrInflateIconView(R.id.icon).apply { - val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme) - val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme) - setIcon(this, LayerDrawable(arrayOf(iconBackground, icon))) - } - childCountAtInflation = childCount } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val containerWidth = MeasureSpec.getSize(widthMeasureSpec) - var containerHeight = MeasureSpec.getSize(heightMeasureSpec) - setMeasuredDimension(containerWidth, containerHeight) + override fun inflateViewStubs() { + findViewById(R.id.icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + } + private fun positionTaskWindows() { if (taskContainers.isEmpty()) { return } val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx - containerHeight -= thumbnailTopMarginPx + + val taskViewWidth = layoutParams.width + val taskViewHeight = layoutParams.height - thumbnailTopMarginPx BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) - val windowWidth = tempPointF.x.toInt() - val windowHeight = tempPointF.y.toInt() - val scaleWidth = containerWidth / windowWidth.toFloat() - val scaleHeight = containerHeight / windowHeight.toFloat() - if (DEBUG) { - Log.d( - TAG, - "onMeasure: container=[$containerWidth,$containerHeight] " + - "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]" - ) - } - // Desktop tile is a shrunk down version of launcher and freeform task thumbnails. + val screenWidth = tempPointF.x.toInt() + val screenHeight = tempPointF.y.toInt() + val screenRect = Rect(0, 0, screenWidth, screenHeight) + val scaleWidth = taskViewWidth / screenWidth.toFloat() + val scaleHeight = taskViewHeight / screenHeight.toFloat() + taskContainers.forEach { - // Default to quarter of the desktop if we did not get app bounds. - val taskSize = - it.task.appBounds - ?: tempRect.apply { - left = 0 - top = 0 - right = windowWidth / 4 - bottom = windowHeight / 4 - } - val thumbWidth = (taskSize.width() * scaleWidth).toInt() - val thumbHeight = (taskSize.height() * scaleHeight).toInt() - it.thumbnailViewDeprecated.measure( - MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY) - ) + val taskId = it.task.key.id + val fullscreenTaskPosition = + fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return + val overviewTaskPosition = + if (enableDesktopExplodedView()) { + viewModel!! + .organizedDesktopTaskPositions + .firstOrNull { it.taskId == taskId } + ?.let { organizedPosition -> + TEMP_OVERVIEW_TASK_POSITION.apply { + lerpRect( + fullscreenTaskPosition.bounds, + organizedPosition.bounds, + explodeProgress, + ) + } + } ?: fullscreenTaskPosition.bounds + } else { + fullscreenTaskPosition.bounds + } - // Position the task to the same position as it would be on the desktop - val positionInParent = it.task.positionInParent ?: ORIGIN - val taskX = (positionInParent.x * scaleWidth).toInt() - var taskY = (positionInParent.y * scaleHeight).toInt() - // move task down by margin size - taskY += thumbnailTopMarginPx - it.thumbnailViewDeprecated.x = taskX.toFloat() - it.thumbnailViewDeprecated.y = taskY.toFloat() - if (DEBUG) { - Log.d( - TAG, - "onMeasure: task=${it.task.key} thumb=[$thumbWidth,$thumbHeight]" + - " pos=[$taskX,$taskY]" - ) + if (enableDesktopExplodedView()) { + getRemoteTargetHandle(taskId)?.let { remoteTargetHandle -> + val fromRect = + TEMP_FROM_RECTF.apply { + set(fullscreenTaskPosition.bounds) + scale(scaleWidth) + offset( + lastComputedTaskSize.left.toFloat(), + lastComputedTaskSize.top.toFloat(), + ) + } + val toRect = + TEMP_TO_RECTF.apply { + set(overviewTaskPosition) + scale(scaleWidth) + offset( + lastComputedTaskSize.left.toFloat(), + lastComputedTaskSize.top.toFloat(), + ) + } + val transform = Matrix() + transform.setRectToRect(fromRect, toRect, Matrix.ScaleToFit.FILL) + remoteTargetHandle.taskViewSimulator.setTaskRectTransform(transform) + remoteTargetHandle.taskViewSimulator.apply(remoteTargetHandle.transformParams) + } + } + + val taskLeft = overviewTaskPosition.left * scaleWidth + val taskTop = overviewTaskPosition.top * scaleHeight + val taskWidth = overviewTaskPosition.width() * scaleWidth + val taskHeight = overviewTaskPosition.height() * scaleHeight + // TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1. + // To run the explode animation in reverse, it may be simpler to use translation/scale + // for all cases where the progress is non-zero. + if (explodeProgress == 0.0f || explodeProgress == 1.0f) { + // Reset scaling and translation that may have been applied during animation. + it.snapshotView.apply { + scaleX = 1.0f + scaleY = 1.0f + translationX = 0.0f + translationY = 0.0f + } + + // Position the task to the same position as it would be on the desktop + it.snapshotView.updateLayoutParams { + gravity = Gravity.LEFT or Gravity.TOP + width = taskWidth.toInt() + height = taskHeight.toInt() + leftMargin = taskLeft.toInt() + topMargin = taskTop.toInt() + } + + if ( + enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail() + ) { + it.thumbnailView.outlineBounds = + if (intersects(overviewTaskPosition, screenRect)) + Rect(overviewTaskPosition).apply { + intersectUnchecked(screenRect) + // Offset to 0,0 to transform into TaskThumbnailView's coordinate + // system. + offset(-overviewTaskPosition.left, -overviewTaskPosition.top) + left = (left * scaleWidth).roundToInt() + top = (top * scaleHeight).roundToInt() + right = (right * scaleWidth).roundToInt() + bottom = (bottom * scaleHeight).roundToInt() + } + else null + } + } else { + // During the animation, apply translation and scale such that the view is + // transformed to where we want, without triggering layout. + it.snapshotView.apply { + pivotX = 0.0f + pivotY = 0.0f + translationX = taskLeft - left + translationY = taskTop - top + scaleX = taskWidth / width.toFloat() + scaleY = taskHeight / height.toFloat() + } } } } - override fun onRecycle() { - super.onRecycle() - visibility = VISIBLE - } - /** Updates this desktop task to the gives task list defined in `tasks` */ fun bind( - tasks: List, + desktopTask: DesktopTask, orientedState: RecentsOrientedState, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ) { + this.desktopTask = desktopTask + // TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view + // support. + // Minimized tasks should not be shown in Overview. + val tasks = desktopTask.tasks.filterNot { it.isMinimized } if (DEBUG) { val sb = StringBuilder() sb.append("bind tasks=").append(tasks.size).append("\n") tasks.forEach { sb.append(" key=${it.key}\n") } Log.d(TAG, sb.toString()) } + cancelPendingLoadTasks() + val backgroundViewIndex = contentView.indexOfChild(backgroundView) + taskContainers = + tasks.map { task -> + val snapshotView = + if (enableRefactorTaskThumbnail()) { + taskThumbnailViewPool!!.view + } else { + taskThumbnailViewDeprecatedPool!!.view + } + contentView.addView(snapshotView, backgroundViewIndex + 1) - if (!isTaskContainersInitialized()) { - taskContainers = arrayListOf() - } - val taskContainers = taskContainers as ArrayList - taskContainers.ensureCapacity(tasks.size) - tasks.forEachIndexed { index, task -> - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated - if (index >= taskContainers.size) { - thumbnailViewDeprecated = taskThumbnailViewPool.view - // Add thumbnailView from to position after the initial child views. - addView( - thumbnailViewDeprecated, - childCountAtInflation, - LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - ) - } else { - thumbnailViewDeprecated = taskContainers[index].thumbnailViewDeprecated - } - val taskContainer = TaskContainer( - task, - // TODO(b/338360089): Support new TTV for DesktopTaskView - thumbnailView = null, - thumbnailViewDeprecated, - iconView, - TransformingTouchDelegate(iconView.asView()), - SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, - digitalWellBeingToast = null, - showWindowsView = null, - taskOverlayFactory - ) - .apply { thumbnailViewDeprecated.bind(task, overlay) } - if (index >= taskContainers.size) { - taskContainers.add(taskContainer) - } else { - taskContainers[index] = taskContainer + this, + task, + snapshotView, + iconView, + TransformingTouchDelegate(iconView.asView()), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + taskOverlayFactory, + ) } - } - repeat(taskContainers.size - tasks.size) { - with(taskContainers.removeLast()) { - removeView(thumbnailViewDeprecated) - taskThumbnailViewPool.recycle(thumbnailViewDeprecated) - } - } - - setOrientationState(orientedState) + onBind(orientedState) } - override fun needsUpdate(dataChange: Int, flag: Int) = - if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false + override fun onBind(orientedState: RecentsOrientedState) { + super.onBind(orientedState) + + if (enableRefactorTaskThumbnail()) { + viewModel = + DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get(context)) + } + } + + override fun onRecycle() { + super.onRecycle() + desktopTask = null + explodeProgress = 0.0f + viewModel = null + visibility = VISIBLE + taskContainers.forEach { removeAndRecycleThumbnailView(it) } + } + + override fun setOrientationState(orientationState: RecentsOrientedState) { + super.setOrientationState(orientationState) + iconView.setIconOrientation(orientationState, isGridTask) + } + + @SuppressLint("RtlHardcoded") + override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) { + super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize) + this.lastComputedTaskSize.set(lastComputedTaskSize) + + updateTaskPositions() + } + + override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { + super.onTaskListVisibilityChanged(visible, changes) + if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { + contentViewFullscreenParams.updateCornerRadius(context) + } + } + + override fun onIconLoaded(taskContainer: TaskContainer) { + // Update contentDescription of snapshotView only, individual task icon is unused. + taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription + } + + override fun setIconState(container: TaskContainer, state: TaskData?) { + container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription + } + + // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon + override fun onIconUnloaded(taskContainer: TaskContainer) {} // thumbnailView is laid out differently and is handled in onMeasure override fun updateThumbnailSize() {} override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { if (relativeToDragLayer) { - container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds) + container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds) } else { - bounds.set(backgroundView) + bounds.set(contentView) } } - override fun launchTaskAnimated(): RunnableList? { + private fun launchTaskWithDesktopController(animated: Boolean): RunnableList? { val recentsView = recentsView ?: return null + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "launchDesktopFromRecents", + taskIds.contentToString(), + ) val endCallback = RunnableList() val desktopController = recentsView.desktopRecentsController checkNotNull(desktopController) { "recentsController is null" } - desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() } - Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}") + desktopController.launchDesktopFromRecents(this, animated) { + endCallback.executeAllAndDestroy() + } + Log.d( + TAG, + "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated", + ) // Callbacks get run from recentsView for case when recents animation already running recentsView.addSideTaskLaunchCallback(endCallback) return endCallback } - override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { - launchTasks() - callback(true) - } + override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true) - // Desktop tile can't be in split screen - override fun confirmSecondSplitSelectApp(): Boolean = false + override fun launchWithoutAnimation( + isQuickSwitch: Boolean, + callback: (launched: Boolean) -> Unit, + ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false) + + // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip + // putting DesktopTaskView to split as it's not supported. + override fun confirmSecondSplitSelectApp(): Boolean = + recentsView?.canLaunchFullscreenTask() != true // TODO(b/330685808) support overlay for Screenshot action override fun setOverlayEnabled(overlayEnabled: Boolean) {} override fun onFullscreenProgressChanged(fullscreenProgress: Float) { - // Don't show background while we are transitioning to/from fullscreen - backgroundView.visibility = if (fullscreenProgress > 0) INVISIBLE else VISIBLE + backgroundView.alpha = 1 - fullscreenProgress } - override fun updateCurrentFullscreenParams() { - super.updateCurrentFullscreenParams() - updateFullscreenParams(snapshotDrawParams) + override fun updateFullscreenParams() { + super.updateFullscreenParams() + updateFullscreenParams(contentViewFullscreenParams) + contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius } - override fun getThumbnailFullscreenParams() = snapshotDrawParams + override fun addChildrenForAccessibility(outChildren: ArrayList) { + super.addChildrenForAccessibility(outChildren) + ViewUtils.addAccessibleChildToList(backgroundView, outChildren) + } + + fun removeTaskFromExplodedView(taskId: Int, animate: Boolean) { + if (!enableDesktopExplodedView()) { + Log.e( + TAG, + "removeTaskFromExplodedView called when enableDesktopExplodedView flag is false", + ) + return + } + + // Remove the task's [taskContainer] and its associated Views. + val taskContainer = getTaskContainerById(taskId) ?: return + removeAndRecycleThumbnailView(taskContainer) + taskContainer.destroy() + taskContainers = taskContainers.filterNot { it == taskContainer } + + // Dismiss the current DesktopTaskView if all its windows are closed. + if (taskContainers.isEmpty()) { + recentsView?.dismissTaskView(this, animate, /* removeTask= */ true) + } else { + // Otherwise, re-position the remaining task windows. + // TODO(b/353949276): Implement the re-layout animations. + updateTaskPositions() + } + } + + private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) { + contentView.removeView(taskContainer.snapshotView) + if (enableRefactorTaskThumbnail()) { + taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView) + } else { + taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated) + } + } + + private fun updateTaskPositions() { + BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) + val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt()) + DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4) + + fullscreenTaskPositions = + taskContainers.map { + DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS) + } + + if (enableDesktopExplodedView()) { + viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions) + } + positionTaskWindows() + } companion object { private const val TAG = "DesktopTaskView" private const val DEBUG = false - private const val VIEW_POOL_MAX_SIZE = 10 + private const val VIEW_POOL_MAX_SIZE = 5 + // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool. private const val VIEW_POOL_INITIAL_SIZE = 0 - private val ORIGIN = Point(0, 0) + private val DEFAULT_BOUNDS = Rect() + // Temporaries used for various purposes to avoid allocations. + private val TEMP_OVERVIEW_TASK_POSITION = Rect() + private val TEMP_FROM_RECTF = RectF() + private val TEMP_TO_RECTF = RectF() } } diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java deleted file mode 100644 index 4a5b9e4cc..000000000 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ /dev/null @@ -1,444 +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.quickstep.views; - -import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS; - -import static com.android.launcher3.Utilities.prefixTextWithIcon; -import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR; - -import android.app.ActivityOptions; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; -import android.content.pm.LauncherApps.AppUsageLimit; -import android.graphics.Outline; -import android.graphics.Paint; -import android.icu.text.MeasureFormat; -import android.icu.text.MeasureFormat.FormatWidth; -import android.icu.util.Measure; -import android.icu.util.MeasureUnit; -import android.os.UserHandle; -import android.util.Log; -import android.util.Pair; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.FrameLayout; -import android.widget.TextView; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; -import com.android.quickstep.TaskUtils; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.systemui.shared.recents.model.Task; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.time.Duration; -import java.util.Locale; - -public final class DigitalWellBeingToast { - - private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f; - private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f; - - /** Will span entire width of taskView with full text */ - private static final int SPLIT_BANNER_FULLSCREEN = 0; - /** Used for grid task view, only showing icon and time */ - private static final int SPLIT_GRID_BANNER_LARGE = 1; - /** Used for grid task view, only showing icon */ - private static final int SPLIT_GRID_BANNER_SMALL = 2; - - @IntDef(value = { - SPLIT_BANNER_FULLSCREEN, - SPLIT_GRID_BANNER_LARGE, - SPLIT_GRID_BANNER_SMALL, - }) - @Retention(RetentionPolicy.SOURCE) - @interface SplitBannerConfig { - } - - static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS); - static final int MINUTE_MS = 60000; - - private static final String TAG = "DigitalWellBeingToast"; - - private final RecentsViewContainer mContainer; - private final TaskView mTaskView; - private final LauncherApps mLauncherApps; - - private final int mBannerHeight; - - private Task mTask; - private boolean mHasLimit; - - private long mAppRemainingTimeMs; - @Nullable - private View mBanner; - private ViewOutlineProvider mOldBannerOutlineProvider; - private float mBannerOffsetPercentage; - @Nullable - private SplitBounds mSplitBounds; - private float mSplitOffsetTranslationY; - private float mSplitOffsetTranslationX; - - private boolean mIsDestroyed = false; - - public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) { - mContainer = container; - mTaskView = taskView; - mLauncherApps = container.asContext().getSystemService(LauncherApps.class); - mBannerHeight = container.asContext().getResources().getDimensionPixelSize( - R.dimen.digital_wellbeing_toast_height); - } - - private void setNoLimit() { - mHasLimit = false; - mTaskView.setContentDescription(mTask.titleDescription); - replaceBanner(null); - mAppRemainingTimeMs = -1; - } - - private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) { - mAppRemainingTimeMs = appRemainingTimeMs; - mHasLimit = true; - TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast, - mContainer.asContext(), mTaskView); - toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top, - getText())); - toast.setOnClickListener(this::openAppUsageSettings); - replaceBanner(toast); - - mTaskView.setContentDescription( - getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs)); - } - - public String getText() { - return getText(mAppRemainingTimeMs, false /* forContentDesc */); - } - - public boolean hasLimit() { - return mHasLimit; - } - - public void initialize(Task task) { - if (mIsDestroyed) { - throw new IllegalStateException("Cannot re-initialize a destroyed toast"); - } - mTask = task; - ORDERED_BG_EXECUTOR.execute(() -> { - AppUsageLimit usageLimit = null; - try { - usageLimit = mLauncherApps.getAppUsageLimit( - mTask.getTopComponent().getPackageName(), - UserHandle.of(mTask.key.userId)); - } catch (Exception e) { - Log.e(TAG, "Error initializing digital well being toast", e); - } - final long appUsageLimitTimeMs = - usageLimit != null ? usageLimit.getTotalUsageLimit() : -1; - final long appRemainingTimeMs = - usageLimit != null ? usageLimit.getUsageRemaining() : -1; - - mTaskView.post(() -> { - if (mIsDestroyed) { - return; - } - if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { - setNoLimit(); - } else { - setLimit(appUsageLimitTimeMs, appRemainingTimeMs); - } - }); - }); - } - - /** - * Mark the DWB toast as destroyed and remove banner from TaskView. - */ - public void destroy() { - mIsDestroyed = true; - mTaskView.post(() -> replaceBanner(null)); - } - - public void setSplitBounds(@Nullable SplitBounds splitBounds) { - mSplitBounds = splitBounds; - } - - private @SplitBannerConfig int getSplitBannerConfig() { - if (mSplitBounds == null - || !mContainer.getDeviceProfile().isTablet - || mTaskView.isFocusedTask()) { - return SPLIT_BANNER_FULLSCREEN; - } - - // For portrait grid only height of task changes, not width. So we keep the text the same - if (!mContainer.getDeviceProfile().isLeftRightSplit) { - return SPLIT_GRID_BANNER_LARGE; - } - - // For landscape grid, for 30% width we only show icon, otherwise show icon and time - if (mTask.key.id == mSplitBounds.leftTopTaskId) { - return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY - ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; - } else { - return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY - ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; - } - } - - private String getReadableDuration( - Duration duration, - @StringRes int durationLessThanOneMinuteStringId) { - int hours = Math.toIntExact(duration.toHours()); - int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes()); - - // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero. - if (hours > 0 && minutes > 0) { - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW) - .formatMeasures( - new Measure(hours, MeasureUnit.HOUR), - new Measure(minutes, MeasureUnit.MINUTE)); - } - - // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). - if (hours > 0) { - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( - new Measure(hours, MeasureUnit.HOUR)); - } - - // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). - if (minutes > 0) { - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( - new Measure(minutes, MeasureUnit.MINUTE)); - } - - // Use a specific string for usage less than one minute but non-zero. - if (duration.compareTo(Duration.ZERO) > 0) { - return mContainer.asContext().getString(durationLessThanOneMinuteStringId); - } - - // Otherwise, return 0-minute string. - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( - new Measure(0, MeasureUnit.MINUTE)); - } - - /** - * Returns text to show for the banner depending on {@link #getSplitBannerConfig()} - * If {@param forContentDesc} is {@code true}, this will always return the full - * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN} - */ - private String getText(long remainingTime, boolean forContentDesc) { - final Duration duration = Duration.ofMillis( - remainingTime > MINUTE_MS ? - (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : - remainingTime); - String readableDuration = getReadableDuration(duration, - R.string.shorter_duration_less_than_one_minute - /* forceFormatWidth */); - @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig(); - if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) { - return mContainer.asContext().getString( - R.string.time_left_for_app, - readableDuration); - } - - if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) { - // show no text - return ""; - } else { // SPLIT_GRID_BANNER_LARGE - // only show time - return readableDuration; - } - } - - public void openAppUsageSettings(View view) { - final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE) - .putExtra(Intent.EXTRA_PACKAGE_NAME, - mTask.getTopComponent().getPackageName()).addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - try { - final RecentsViewContainer container = - RecentsViewContainer.containerFromContext(view.getContext()); - final ActivityOptions options = ActivityOptions.makeScaleUpAnimation( - view, 0, 0, - view.getWidth(), view.getHeight()); - container.asContext().startActivity(intent, options.toBundle()); - - // TODO: add WW logging on the app usage settings click. - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Failed to open app usage settings for task " - + mTask.getTopComponent().getPackageName(), e); - } - } - - private String getContentDescriptionForTask( - Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) { - return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ? - mContainer.asContext().getString( - R.string.task_contents_description_with_remaining_time, - task.titleDescription, - getText(appRemainingTimeMs, true /* forContentDesc */)) : - task.titleDescription; - } - - private void replaceBanner(@Nullable View view) { - resetOldBanner(); - setBanner(view); - } - - private void resetOldBanner() { - if (mBanner != null) { - mBanner.setOutlineProvider(mOldBannerOutlineProvider); - mTaskView.removeView(mBanner); - mBanner.setOnClickListener(null); - mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner); - } - } - - private void setBanner(@Nullable View view) { - mBanner = view; - if (mBanner != null && mTaskView.getRecentsView() != null) { - setupAndAddBanner(); - setBannerOutline(); - } - } - - private void setupAndAddBanner() { - FrameLayout.LayoutParams layoutParams = - (FrameLayout.LayoutParams) mBanner.getLayoutParams(); - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams) - mTaskView.getFirstThumbnailViewDeprecated().getLayoutParams()).bottomMargin; - RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler(); - Pair translations = orientationHandler - .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(), - mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile, - mTaskView.getThumbnailViews(), mTask.key.id, mBanner); - mSplitOffsetTranslationX = translations.first; - mSplitOffsetTranslationY = translations.second; - updateTranslationY(); - updateTranslationX(); - mTaskView.addView(mBanner); - } - - private void setBannerOutline() { - // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null - mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null - ? mBanner.getOutlineProvider() - : ViewOutlineProvider.BACKGROUND; - - mBanner.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - mOldBannerOutlineProvider.getOutline(view, outline); - float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY; - outline.offset(0, Math.round(verticalTranslation)); - } - }); - mBanner.setClipToOutline(true); - } - - void updateBannerOffset(float offsetPercentage) { - if (mBannerOffsetPercentage != offsetPercentage) { - mBannerOffsetPercentage = offsetPercentage; - if (mBanner != null) { - updateTranslationY(); - mBanner.invalidateOutline(); - } - } - } - - private void updateTranslationY() { - if (mBanner == null) { - return; - } - - mBanner.setTranslationY( - (mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY); - } - - private void updateTranslationX() { - if (mBanner == null) { - return; - } - - mBanner.setTranslationX(mSplitOffsetTranslationX); - } - - void setBannerColorTint(int color, float amount) { - if (mBanner == null) { - return; - } - if (amount == 0) { - mBanner.setLayerType(View.LAYER_TYPE_NONE, null); - } - Paint layerPaint = new Paint(); - layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount)); - mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint); - mBanner.setLayerPaint(layerPaint); - } - - void setBannerVisibility(int visibility) { - if (mBanner == null) { - return; - } - - mBanner.setVisibility(visibility); - } - - private int getAccessibilityActionId() { - return (mSplitBounds != null - && mSplitBounds.rightBottomTaskId == mTask.key.id) - ? R.id.action_digital_wellbeing_bottom_right - : R.id.action_digital_wellbeing_top_left; - } - - @Nullable - public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() { - if (!hasLimit()) { - return null; - } - - Context context = mContainer.asContext(); - String label = - (mTaskView.containsMultipleTasks()) - ? context.getString( - R.string.split_app_usage_settings, - TaskUtils.getTitle(context, mTask) - ) : context.getString(R.string.accessibility_app_usage_settings); - return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label); - } - - public boolean handleAccessibilityAction(int action) { - if (getAccessibilityActionId() == action) { - openAppUsageSettings(mTaskView); - return true; - } else { - return false; - } - } -} diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt new file mode 100644 index 000000000..5c4a35da6 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt @@ -0,0 +1,405 @@ +/* + * 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.quickstep.views + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.AppUsageLimit +import android.graphics.Outline +import android.graphics.Paint +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit +import android.os.UserHandle +import android.provider.Settings +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.ViewOutlineProvider +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.core.util.component1 +import androidx.core.util.component2 +import androidx.core.view.isVisible +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.util.Executors +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.quickstep.TaskUtils +import com.android.systemui.shared.recents.model.Task +import java.time.Duration +import java.util.Locale + +@SuppressLint("AppCompatCustomView") +class DigitalWellBeingToast +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : TextView(context, attrs, defStyleAttr, defStyleRes) { + private val recentsViewContainer: RecentsViewContainer = + RecentsViewContainer.containerFromContext(context) + + private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java) + + private val bannerHeight = + context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height) + + private lateinit var task: Task + private lateinit var taskView: TaskView + private lateinit var snapshotView: View + @StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED + + private var appRemainingTimeMs: Long = 0 + private var splitOffsetTranslationY = 0f + set(value) { + if (field != value) { + field = value + updateTranslationY() + } + } + + private var isDestroyed = false + + var hasLimit = false + var splitBounds: SplitConfigurationOptions.SplitBounds? = null + var bannerOffsetPercentage = 0f + set(value) { + if (field != value) { + field = value + updateTranslationY() + } + } + + init { + setOnClickListener(::openAppUsageSettings) + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + BACKGROUND.getOutline(view, outline) + val verticalTranslation = splitOffsetTranslationY - translationY + outline.offset(0, Math.round(verticalTranslation)) + } + } + clipToOutline = true + } + + private fun setNoLimit() { + isVisible = false + hasLimit = false + appRemainingTimeMs = -1 + setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1) + } + + private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) { + isVisible = true + hasLimit = true + this.appRemainingTimeMs = appRemainingTimeMs + setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs) + text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText()) + } + + private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) { + val contentDescription = + getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs) + snapshotView.contentDescription = contentDescription + } + + fun initialize() { + check(!isDestroyed) { "Cannot re-initialize a destroyed toast" } + setupTranslations() + Executors.ORDERED_BG_EXECUTOR.execute { + var usageLimit: AppUsageLimit? = null + try { + usageLimit = + launcherApps?.getAppUsageLimit( + task.topComponent.packageName, + UserHandle.of(task.key.userId), + ) + } catch (e: Exception) { + Log.e(TAG, "Error initializing digital well being toast", e) + } + val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1 + val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1 + + taskView.post { + if (isDestroyed) return@post + if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { + setNoLimit() + } else { + setLimit(appUsageLimitTimeMs, appRemainingTimeMs) + } + } + } + } + + /** Bind the DWB toast to its dependencies. */ + fun bind( + task: Task, + taskView: TaskView, + snapshotView: View, + @StagePosition stagePosition: Int, + ) { + this.task = task + this.taskView = taskView + this.snapshotView = snapshotView + this.stagePosition = stagePosition + isDestroyed = false + } + + /** Mark the DWB toast as destroyed and hide it. */ + fun destroy() { + isVisible = false + isDestroyed = true + } + + private fun getSplitBannerConfig(): SplitBannerConfig { + val splitBounds = splitBounds + return when { + splitBounds == null || + !recentsViewContainer.deviceProfile.isTablet || + taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN + // For portrait grid only height of task changes, not width. So we keep the text the + // same + !recentsViewContainer.deviceProfile.isLeftRightSplit -> + SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + // For landscape grid, for 30% width we only show icon, otherwise show icon and time + task.key.id == splitBounds.leftTopTaskId -> + if (splitBounds.leftTopTaskPercent < THRESHOLD_LEFT_ICON_ONLY) + SplitBannerConfig.SPLIT_GRID_BANNER_SMALL + else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + else -> + if (splitBounds.leftTopTaskPercent > THRESHOLD_RIGHT_ICON_ONLY) + SplitBannerConfig.SPLIT_GRID_BANNER_SMALL + else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + } + } + + private fun getReadableDuration( + duration: Duration, + @StringRes durationLessThanOneMinuteStringId: Int, + ): String { + val hours = Math.toIntExact(duration.toHours()) + val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes()) + return when { + // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero. + hours > 0 && minutes > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW) + .formatMeasures( + Measure(hours, MeasureUnit.HOUR), + Measure(minutes, MeasureUnit.MINUTE), + ) + // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). + hours > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(hours, MeasureUnit.HOUR)) + // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). + minutes > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(minutes, MeasureUnit.MINUTE)) + // Use a specific string for usage less than one minute but non-zero. + duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId) + // Otherwise, return 0-minute string. + else -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(0, MeasureUnit.MINUTE)) + } + } + + /** + * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param + * forContentDesc} is `true`, this will always return the full string corresponding to + * [.SPLIT_BANNER_FULLSCREEN] + */ + @JvmOverloads + @VisibleForTesting + fun getBannerText( + remainingTime: Long = appRemainingTimeMs, + forContentDesc: Boolean = false, + ): String { + val duration = + Duration.ofMillis( + if (remainingTime > MINUTE_MS) + (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS + else remainingTime + ) + val readableDuration = + getReadableDuration( + duration, + R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */ + ) + val splitBannerConfig = getSplitBannerConfig() + return when { + forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN -> + context.getString(R.string.time_left_for_app, readableDuration) + // show no text + splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> "" + // SPLIT_GRID_BANNER_LARGE only show time + else -> readableDuration + } + } + + private fun openAppUsageSettings(view: View) { + val intent = + Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE) + .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + try { + val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height) + context.startActivity(intent, options.toBundle()) + + // TODO: add WW logging on the app usage settings click. + } catch (e: ActivityNotFoundException) { + Log.e( + TAG, + "Failed to open app usage settings for task " + task.topComponent.packageName, + e, + ) + } + } + + private fun getContentDescriptionForTask( + task: Task, + appUsageLimitTimeMs: Long, + appRemainingTimeMs: Long, + ): String? = + if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0) + context.getString( + R.string.task_contents_description_with_remaining_time, + task.titleDescription, + getBannerText(appRemainingTimeMs, true /* forContentDesc */), + ) + else task.titleDescription + + fun setupLayout() { + val snapshotWidth: Int + val snapshotHeight: Int + val splitBounds = splitBounds + if (splitBounds == null) { + snapshotWidth = taskView.layoutParams.width + snapshotHeight = + taskView.layoutParams.height - + recentsViewContainer.deviceProfile.overviewTaskThumbnailTopMarginPx + } else { + val groupedTaskSize = + taskView.pagedOrientationHandler.getGroupedTaskViewSizes( + recentsViewContainer.deviceProfile, + splitBounds, + taskView.layoutParams.width, + taskView.layoutParams.height, + ) + if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) { + snapshotWidth = groupedTaskSize.first.x + snapshotHeight = groupedTaskSize.first.y + } else { + snapshotWidth = groupedTaskSize.second.x + snapshotHeight = groupedTaskSize.second.y + } + } + taskView.pagedOrientationHandler.updateDwbBannerLayout( + taskView.layoutParams.width, + taskView.layoutParams.height, + taskView is GroupedTaskView, + recentsViewContainer.deviceProfile, + snapshotWidth, + snapshotHeight, + this, + ) + } + + private fun setupTranslations() { + val (translationX, translationY) = + taskView.pagedOrientationHandler.getDwbBannerTranslations( + taskView.layoutParams.width, + taskView.layoutParams.height, + splitBounds, + recentsViewContainer.deviceProfile, + taskView.snapshotViews, + task.key.id, + this, + ) + this.translationX = translationX + this.splitOffsetTranslationY = translationY + } + + private fun updateTranslationY() { + translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY + invalidateOutline() + } + + fun setColorTint(color: Int, amount: Float) { + if (amount == 0f) { + setLayerType(View.LAYER_TYPE_NONE, null) + } + val layerPaint = Paint() + layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount)) + setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint) + setLayerPaint(layerPaint) + } + + private fun getAccessibilityActionId(): Int = + if (splitBounds?.rightBottomTaskId == task.key.id) + R.id.action_digital_wellbeing_bottom_right + else R.id.action_digital_wellbeing_top_left + + fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? { + if (!hasLimit) return null + val label = + if (taskView.containsMultipleTasks()) + context.getString( + R.string.split_app_usage_settings, + TaskUtils.getTitle(context, task), + ) + else context.getString(R.string.accessibility_app_usage_settings) + return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label) + } + + fun handleAccessibilityAction(action: Int): Boolean { + if (getAccessibilityActionId() != action) return false + openAppUsageSettings(taskView) + return true + } + + companion object { + private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f + private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f + + enum class SplitBannerConfig { + /** Will span entire width of taskView with full text */ + SPLIT_BANNER_FULLSCREEN, + /** Used for grid task view, only showing icon and time */ + SPLIT_GRID_BANNER_LARGE, + /** Used for grid task view, only showing icon */ + SPLIT_GRID_BANNER_SMALL, + } + + val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS) + const val MINUTE_MS: Int = 60000 + + private const val TAG = "DigitalWellBeingToast" + } +} diff --git a/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt new file mode 100644 index 000000000..c8930165b --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt @@ -0,0 +1,56 @@ +/* + * 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.quickstep.views + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.ImageView + +/** + * An [ImageView] that does not requestLayout() unless setLayoutParams is called. + * + * This is useful, particularly during animations, for [ImageView]s that are not supposed to be + * resized. + */ +@SuppressLint("AppCompatCustomView") +class FixedSizeImageView : ImageView { + private var shouldRequestLayoutOnChanges = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + override fun setLayoutParams(params: ViewGroup.LayoutParams?) { + shouldRequestLayoutOnChanges = true + super.setLayoutParams(params) + shouldRequestLayoutOnChanges = false + } + + override fun requestLayout() { + if (shouldRequestLayoutOnChanges) { + super.requestLayout() + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt index e024995aa..6bbd6b2c1 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt @@ -57,6 +57,7 @@ open class FloatingAppPairBackground( private val container: RecentsViewContainer private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG) // Animation interpolators protected val expandXInterpolator: Interpolator @@ -105,13 +106,15 @@ open class FloatingAppPairBackground( ) // Find device-specific measurements - deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext()) + val resources = context.resources + deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context) deviceHalfDividerSize = - container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f + resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f val dividerCenterPos = dividerPos + deviceHalfDividerSize desiredSplitRatio = if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx else dividerCenterPos / dp.heightPx + dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/) } override fun draw(canvas: Canvas) { @@ -153,8 +156,12 @@ open class FloatingAppPairBackground( val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height) // The right half of the background image val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height) + // Middle part is for divider background + val middleRect = RectF(leftSide.right - deviceHalfDividerSize, 0f, + rightSide.left + deviceHalfDividerSize, height) // Draw background + canvas.drawRect(middleRect, dividerPaint) drawCustomRoundedRect( canvas, leftSide, @@ -251,8 +258,12 @@ open class FloatingAppPairBackground( val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize) // The bottom half of the background image val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height) + // Middle part is for divider background + val middleRect = RectF(0f, topSide.bottom - deviceHalfDividerSize, + width, bottomSide.top + deviceHalfDividerSize) // Draw background + canvas.drawRect(middleRect, dividerPaint) drawCustomRoundedRect( canvas, topSide, diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java index 4ea77532b..f17be05d4 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java @@ -32,7 +32,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.launcher3.widget.LauncherAppWidgetHostView; -import com.android.launcher3.widget.RoundedCornerEnforcement; import java.util.stream.IntStream; @@ -171,8 +170,7 @@ final class FloatingWidgetBackgroundView extends View { /** Corner radius from source view's outline, or enforced view. */ private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) { - if (RoundedCornerEnforcement.isRoundedCornerEnabled() - && hostView.hasEnforcedCornerRadius()) { + if (hostView.hasEnforcedCornerRadius()) { return hostView.getEnforcedCornerRadius(); } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider && v.getClipToOutline()) { diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java index 4dde635da..b719ee5b7 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java @@ -18,6 +18,7 @@ package com.android.quickstep.views; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.annotation.TargetApi; +import android.app.TaskInfo; import android.content.Context; import android.graphics.Matrix; import android.graphics.RectF; @@ -123,8 +124,8 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, @Override public void onGlobalLayout() { if (isUninitialized()) return; - positionViews(); - if (mOnTargetChangeRunnable != null) { + boolean positionsChanged = positionViews(); + if (mOnTargetChangeRunnable != null && positionsChanged) { mOnTargetChangeRunnable.run(); } } @@ -212,21 +213,43 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, onGlobalLayout(); } - /** Sets the layout parameters of the floating view and its background view child. */ - private void positionViews() { + /** + * Sets the layout parameters of the floating view and its background view child. + * @return true if any of the views positions change due to this call. + */ + private boolean positionViews() { + boolean positionsChanged = false; + LayoutParams layoutParams = (LayoutParams) getLayoutParams(); - layoutParams.setMargins(0, 0, 0, 0); - setLayoutParams(layoutParams); + + if (layoutParams.topMargin != 0 || layoutParams.bottomMargin != 0 + || layoutParams.rightMargin != 0 || layoutParams.leftMargin != 0) { + positionsChanged = true; + layoutParams.setMargins(0, 0, 0, 0); + setLayoutParams(layoutParams); + } // FloatingWidgetView layout is forced LTR - mBackgroundView.setTranslationX(mBackgroundPosition.left); - mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY); + float targetY = mBackgroundPosition.top + mIconOffsetY; + if (mBackgroundView.getTranslationX() != mBackgroundPosition.left + || mBackgroundView.getTranslationY() != targetY) { + positionsChanged = true; + mBackgroundView.setTranslationX(mBackgroundPosition.left); + mBackgroundView.setTranslationY(targetY); + } + LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams(); - backgroundParams.leftMargin = 0; - backgroundParams.topMargin = 0; - backgroundParams.width = (int) mBackgroundPosition.width(); - backgroundParams.height = (int) mBackgroundPosition.height(); - mBackgroundView.setLayoutParams(backgroundParams); + if (backgroundParams.leftMargin != 0 || backgroundParams.topMargin != 0 + || backgroundParams.width != Math.round(mBackgroundPosition.width()) + || backgroundParams.height != Math.round(mBackgroundPosition.height())) { + positionsChanged = true; + + backgroundParams.leftMargin = 0; + backgroundParams.topMargin = 0; + backgroundParams.width = Math.round(mBackgroundPosition.width()); + backgroundParams.height = Math.round(mBackgroundPosition.height()); + mBackgroundView.setLayoutParams(backgroundParams); + } if (mForegroundOverlayView != null) { sTmpMatrix.reset(); @@ -237,8 +260,15 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, sTmpMatrix.postScale(foregroundScale, foregroundScale); sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top + mIconOffsetY); - mForegroundOverlayView.setMatrix(sTmpMatrix); + + // We use the animation matrix here, because calling setMatrix on the GhostView + // actually sets the animation matrix, not the regular one. + if (!sTmpMatrix.equals(mForegroundOverlayView.getAnimationMatrix())) { + positionsChanged = true; + mForegroundOverlayView.setMatrix(sTmpMatrix); + } } + return positionsChanged; } private void finish(DragLayer dragLayer) { @@ -304,11 +334,16 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, * context's theme background color. */ public static int getDefaultBackgroundColor( - Context context, RemoteAnimationTarget target) { - return (target != null && target.taskInfo != null - && target.taskInfo.taskDescription != null) - ? target.taskInfo.taskDescription.getBackgroundColor() - : Themes.getColorBackground(context); + Context context, @Nullable RemoteAnimationTarget target) { + final int fallbackColor = Themes.getColorBackground(context); + if (target == null) { + return fallbackColor; + } + final TaskInfo taskInfo = target.taskInfo; + if (taskInfo == null) { + return fallbackColor; + } + return taskInfo.taskDescription.getBackgroundColor(); } private static void getRelativePosition(View descendant, View ancestor, RectF position) { diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index d6a3376c5..faa9e2893 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -21,11 +21,12 @@ import android.graphics.PointF import android.util.AttributeSet import android.util.Log import android.view.View +import android.view.ViewStub import com.android.internal.jank.Cuj import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R import com.android.launcher3.Utilities -import com.android.launcher3.config.FeatureFlags import com.android.launcher3.util.RunnableList import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT @@ -33,12 +34,11 @@ import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_O import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.util.RecentsOrientedState -import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell import com.android.quickstep.util.SplitSelectStateController import com.android.systemui.shared.recents.model.Task -import com.android.systemui.shared.recents.utilities.PreviewPositionHelper import com.android.systemui.shared.system.InteractionJankMonitorWrapper -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition +import com.android.wm.shell.Flags.enableFlexibleTwoAppSplit +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition /** * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks @@ -51,7 +51,16 @@ import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosi * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included). */ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - TaskView(context, attrs) { + TaskView(context, attrs, type = TaskViewType.GROUPED) { + + private val MINIMUM_RATIO_TO_SHOW_ICON = 0.2f + + val leftTopTaskContainer: TaskContainer + get() = taskContainers[0] + + val rightBottomTaskContainer: TaskContainer + get() = taskContainers[1] + // TODO(b/336612373): Support new TTV for GroupedTaskView var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null private set @@ -67,52 +76,41 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val heightSize = MeasureSpec.getSize(heightMeasureSpec) setMeasuredDimension(widthSize, heightSize) val splitBoundsConfig = splitBoundsConfig ?: return - val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() - if (initSplitTaskId == INVALID_TASK_ID) { - pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( - taskContainers[0].thumbnailViewDeprecated, - taskContainers[1].thumbnailViewDeprecated, - widthSize, - heightSize, - splitBoundsConfig, - container.deviceProfile, - layoutDirection == LAYOUT_DIRECTION_RTL - ) - // Should we be having a separate translation step apart from the measuring above? - // The following only applies to large screen for now, but for future reference - // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary - // translation directions - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX) - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY) - } else { - // Currently being split with this taskView, let the non-split selected thumbnail - // take up full thumbnail area - taskContainers - .firstOrNull { it.task.key.id != initSplitTaskId } - ?.thumbnailViewDeprecated - ?.measure( - widthMeasureSpec, - MeasureSpec.makeMeasureSpec( - heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx, - MeasureSpec.EXACTLY - ) - ) - } + val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID + pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( + leftTopTaskContainer.snapshotView, + rightBottomTaskContainer.snapshotView, + widthSize, + heightSize, + splitBoundsConfig, + container.deviceProfile, + layoutDirection == LAYOUT_DIRECTION_RTL, + inSplitSelection, + ) + if (!enableOverviewIconMenu()) { updateIconPlacement() } } + override fun inflateViewStubs() { + super.inflateViewStubs() + findViewById(R.id.bottomright_snapshot) + ?.apply { + layoutResource = + if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail + else R.layout.task_thumbnail_deprecated + } + ?.inflate() + findViewById(R.id.bottomRight_icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + } + override fun onRecycle() { super.onRecycle() splitBoundsConfig = null @@ -133,37 +131,23 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu R.id.snapshot, R.id.icon, R.id.show_windows, + R.id.digital_wellbeing_toast, STAGE_POSITION_TOP_OR_LEFT, - taskOverlayFactory + taskOverlayFactory, ), createTaskContainer( secondaryTask, R.id.bottomright_snapshot, R.id.bottomRight_icon, R.id.show_windows_right, + R.id.bottomRight_digital_wellbeing_toast, STAGE_POSITION_BOTTOM_OR_RIGHT, - taskOverlayFactory - ) + taskOverlayFactory, + ), ) - this.splitBoundsConfig = - splitBoundsConfig?.also { - taskContainers[0] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT - ) - taskContainers[1] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT - ) - } - taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } - setOrientationState(orientedState) + this.splitBoundsConfig = splitBoundsConfig + taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig } + onBind(orientedState) } override fun setOrientationState(orientationState: RecentsOrientedState) { @@ -174,7 +158,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu container.deviceProfile, it, layoutParams.width, - layoutParams.height + layoutParams.height, ) val iconViewMarginStart = resources.getDimensionPixelSize( @@ -187,12 +171,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2 // setMaxWidth() needs to be called before mIconView.setIconOrientation which is // called in the super below. - (taskContainers[0].iconView as IconAppChipView).setMaxWidth( + (leftTopTaskContainer.iconView as IconAppChipView).maxWidth = groupedTaskViewSizes.first.x - iconMargins - ) - (taskContainers[1].iconView as IconAppChipView).setMaxWidth( + (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth = groupedTaskViewSizes.second.x - iconMargins - ) } } super.setOrientationState(orientationState) @@ -201,40 +183,72 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu private fun updateIconPlacement() { val splitBoundsConfig = splitBoundsConfig ?: return - val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx - val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + val deviceProfile = container.deviceProfile + val taskIconHeight = deviceProfile.overviewTaskIconSizePx + val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID + var oneIconHiddenDueToSmallWidth = false + + if (enableFlexibleTwoAppSplit()) { + // Update values for both icons' setFlexSplitAlpha. Mainly, we want to hide an icon if + // its app tile is too small. But we also have to set the alphas back if we go to + // split selection. + val hideLeftTopIcon: Boolean + val hideRightBottomIcon: Boolean + if (inSplitSelection) { + hideLeftTopIcon = + getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.leftTopTaskId + hideRightBottomIcon = + getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.rightBottomTaskId + } else { + hideLeftTopIcon = splitBoundsConfig.leftTopTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON + hideRightBottomIcon = + splitBoundsConfig.rightBottomTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON + if (hideLeftTopIcon || hideRightBottomIcon) { + oneIconHiddenDueToSmallWidth = true + } + } + + leftTopTaskContainer.iconView.setFlexSplitAlpha(if (hideLeftTopIcon) 0f else 1f) + rightBottomTaskContainer.iconView.setFlexSplitAlpha(if (hideRightBottomIcon) 0f else 1f) + } + if (enableOverviewIconMenu()) { + val isDeviceRtl = Utilities.isRtl(resources) val groupedTaskViewSizes = pagedOrientationHandler.getGroupedTaskViewSizes( - container.deviceProfile, + deviceProfile, splitBoundsConfig, layoutParams.width, - layoutParams.height + layoutParams.height, ) pagedOrientationHandler.setSplitIconParams( - taskContainers[0].iconView.asView(), - taskContainers[1].iconView.asView(), + leftTopTaskContainer.iconView.asView(), + rightBottomTaskContainer.iconView.asView(), taskIconHeight, groupedTaskViewSizes.first.x, groupedTaskViewSizes.first.y, layoutParams.height, layoutParams.width, - isRtl, - container.deviceProfile, - splitBoundsConfig + isDeviceRtl, + deviceProfile, + splitBoundsConfig, + inSplitSelection, + oneIconHiddenDueToSmallWidth, ) } else { pagedOrientationHandler.setSplitIconParams( - taskContainers[0].iconView.asView(), - taskContainers[1].iconView.asView(), + leftTopTaskContainer.iconView.asView(), + rightBottomTaskContainer.iconView.asView(), taskIconHeight, - taskContainers[0].thumbnailViewDeprecated.measuredWidth, - taskContainers[0].thumbnailViewDeprecated.measuredHeight, + leftTopTaskContainer.snapshotView.measuredWidth, + leftTopTaskContainer.snapshotView.measuredHeight, measuredHeight, measuredWidth, - isRtl, - container.deviceProfile, - splitBoundsConfig + isLayoutRtl, + deviceProfile, + splitBoundsConfig, + inSplitSelection, + oneIconHiddenDueToSmallWidth, ) } } @@ -242,24 +256,20 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { splitBoundsConfig = splitBounds taskContainers.forEach { - it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) - it.digitalWellBeingToast?.initialize(it.task) + it.digitalWellBeingToast?.splitBounds = splitBoundsConfig + it.digitalWellBeingToast?.initialize() } invalidate() } - override fun launchTaskAnimated(): RunnableList? { - if (taskContainers.isEmpty()) { - Log.d(TAG, "launchTaskAnimated - task is not bound") - return null - } + override fun launchAsStaticTile(): RunnableList? { val recentsView = recentsView ?: return null val endCallback = RunnableList() // Callbacks run from remote animation when recents animation not currently running InteractionJankMonitorWrapper.begin( this, Cuj.CUJ_SPLIT_SCREEN_ENTER, - "Enter form GroupedTaskView" + "Enter form GroupedTaskView", ) launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) { endCallback.executeAllAndDestroy() @@ -271,8 +281,11 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu return endCallback } - override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { - launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/) + override fun launchWithoutAnimation( + isQuickSwitch: Boolean, + callback: (launched: Boolean) -> Unit, + ) { + launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback) } /** @@ -284,19 +297,22 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu private fun launchTaskInternal( isQuickSwitch: Boolean, launchingExistingTaskView: Boolean, - callback: (launched: Boolean) -> Unit + callback: (launched: Boolean) -> Unit, ) { recentsView?.let { it.splitSelectController.launchExistingSplitPair( if (launchingExistingTaskView) this else null, - taskContainers[0].task.key.id, - taskContainers[1].task.key.id, + leftTopTaskContainer.task.key.id, + rightBottomTaskContainer.task.key.id, STAGE_POSITION_TOP_OR_LEFT, callback, isQuickSwitch, - snapPosition + snapPosition, + ) + Log.d( + TAG, + "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView", ) - Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}") } } @@ -317,14 +333,14 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu // checks below aren't reliable since both of those views may be gone/transformed val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() if (initSplitTaskId != INVALID_TASK_ID) { - return if (initSplitTaskId == taskContainers[0].task.key.id) 1 else 0 + return if (initSplitTaskId == leftTopTaskContainer.task.key.id) 1 else 0 } } // Check which of the two apps was selected if ( - taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) || - taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition) + rightBottomTaskContainer.iconView.asView().containsPoint(lastTouchDownPosition) || + rightBottomTaskContainer.snapshotView.containsPoint(lastTouchDownPosition) ) { return 1 } @@ -337,14 +353,6 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu return Utilities.pointInView(this, localPos[0], localPos[1], 0f /* slop */) } - override fun setOverlayEnabled(overlayEnabled: Boolean) { - if (FeatureFlags.enableAppPairs()) { - super.setOverlayEnabled(overlayEnabled) - } else { - // Intentional no-op to prevent setting smart actions overlay on thumbnails - } - } - companion object { private const val TAG = "GroupedTaskView" } diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java deleted file mode 100644 index ba42594e9..000000000 --- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java +++ /dev/null @@ -1,464 +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.quickstep.views; - -import static com.android.app.animation.Interpolators.EMPHASIZED; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Outline; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewOutlineProvider; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MultiPropertyFactory; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.RecentsOrientedState; - -/** - * An icon app menu view which can be used in place of an IconView in overview TaskViews. - */ -public class IconAppChipView extends FrameLayout implements TaskViewIcon { - - private static final int MENU_BACKGROUND_REVEAL_DURATION = 417; - private static final int MENU_BACKGROUND_HIDE_DURATION = 333; - - private static final int NUM_ALPHA_CHANNELS = 3; - private static final int INDEX_CONTENT_ALPHA = 0; - private static final int INDEX_COLOR_FILTER_ALPHA = 1; - private static final int INDEX_MODAL_ALPHA = 2; - - private final MultiValueAlpha mMultiValueAlpha; - - private View mMenuAnchorView; - private IconView mIconView; - // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. - private TextView mIconTextCollapsedView; - private TextView mIconTextExpandedView; - private ImageView mIconArrowView; - private final Rect mBackgroundRelativeLtrLocation = new Rect(); - final RectEvaluator mBackgroundAnimationRectEvaluator = - new RectEvaluator(mBackgroundRelativeLtrLocation); - private final int mCollapsedMenuDefaultWidth; - private final int mExpandedMenuDefaultWidth; - private final int mCollapsedMenuDefaultHeight; - private final int mExpandedMenuDefaultHeight; - private final int mIconMenuMarginTopStart; - private final int mMenuToChipGap; - private final int mBackgroundMarginTopStart; - private final int mAppNameHorizontalMargin; - private final int mIconViewMarginStart; - private final int mAppIconSize; - private final int mArrowSize; - private final int mIconViewDrawableExpandedSize; - private final int mArrowMarginEnd; - private AnimatorSet mAnimator; - - private int mMaxWidth = Integer.MAX_VALUE; - - private static final int INDEX_SPLIT_TRANSLATION = 0; - private static final int INDEX_MENU_TRANSLATION = 1; - private static final int INDEX_COUNT_TRANSLATION = 2; - - private final MultiPropertyFactory mViewTranslationX; - private final MultiPropertyFactory mViewTranslationY; - - /** - * Gets the view split x-axis translation - */ - public MultiPropertyFactory.MultiProperty getSplitTranslationX() { - return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION); - } - - /** - * Sets the view split x-axis translation - * @param translationX x-axis translation - */ - public void setSplitTranslationX(float translationX) { - getSplitTranslationX().setValue(translationX); - } - - /** - * Gets the view split y-axis translation - */ - public MultiPropertyFactory.MultiProperty getSplitTranslationY() { - return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION); - } - - /** - * Sets the view split y-axis translation - * @param translationY y-axis translation - */ - public void setSplitTranslationY(float translationY) { - getSplitTranslationY().setValue(translationY); - } - - /** - * Gets the menu x-axis translation for split task - */ - public MultiPropertyFactory.MultiProperty getMenuTranslationX() { - return mViewTranslationX.get(INDEX_MENU_TRANSLATION); - } - - /** - * Gets the menu y-axis translation for split task - */ - public MultiPropertyFactory.MultiProperty getMenuTranslationY() { - return mViewTranslationY.get(INDEX_MENU_TRANSLATION); - } - - public IconAppChipView(Context context) { - this(context, null); - } - - public IconAppChipView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - Resources res = getResources(); - mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS); - mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true); - - // Menu dimensions - mCollapsedMenuDefaultWidth = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width); - mExpandedMenuDefaultWidth = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width); - mCollapsedMenuDefaultHeight = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height); - mExpandedMenuDefaultHeight = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height); - mIconMenuMarginTopStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin); - mMenuToChipGap = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_gap); - - // Background dimensions - mBackgroundMarginTopStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_background_margin_top_start); - - // Contents dimensions - mAppNameHorizontalMargin = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed); - mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin); - mIconViewMarginStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_view_start_margin); - mAppIconSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size); - mArrowSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_arrow_size); - mIconViewDrawableExpandedSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size); - - mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X, - INDEX_COUNT_TRANSLATION, - Float::sum); - mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y, - INDEX_COUNT_TRANSLATION, - Float::sum); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mIconView = findViewById(R.id.icon_view); - mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed); - mIconTextExpandedView = findViewById(R.id.icon_text_expanded); - mIconArrowView = findViewById(R.id.icon_arrow); - mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor); - } - - protected IconView getIconView() { - return mIconView; - } - - @Override - public void setText(CharSequence text) { - if (mIconTextCollapsedView != null) { - mIconTextCollapsedView.setText(text); - } - if (mIconTextExpandedView != null) { - mIconTextExpandedView.setText(text); - } - } - - @Override - public Drawable getDrawable() { - return mIconView == null ? null : mIconView.getDrawable(); - } - - @Override - public void setDrawable(Drawable icon) { - if (mIconView != null) { - mIconView.setDrawable(icon); - } - } - - @Override - public void setDrawableSize(int iconWidth, int iconHeight) { - if (mIconView != null) { - mIconView.setDrawableSize(iconWidth, iconHeight); - } - } - - /** - * Sets the maximum width of this Icon Menu. This is usually used when space is limited for - * split screen. - */ - public void setMaxWidth(int maxWidth) { - // Width showing only the app icon and arrow. Max width should not be set to less than this. - int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd; - mMaxWidth = Math.max(maxWidth, minimumMaxWidth); - } - - @Override - public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) { - RecentsPagedOrientationHandler orientationHandler = - orientationState.getOrientationHandler(); - // Layout params for anchor view - LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams(); - anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap; - mMenuAnchorView.setLayoutParams(anchorLayoutParams); - - // Layout Params for the Menu View (this) - LayoutParams iconMenuParams = (LayoutParams) getLayoutParams(); - iconMenuParams.width = mExpandedMenuDefaultWidth; - iconMenuParams.height = mExpandedMenuDefaultHeight; - orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart, - mIconMenuMarginTopStart); - setLayoutParams(iconMenuParams); - - // Layout params for the background - Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); - mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds); - setOutlineProvider(new ViewOutlineProvider() { - final Rect mRtlAppliedOutlineBounds = new Rect(); - @Override - public void getOutline(View view, Outline outline) { - mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation); - if (isLayoutRtl()) { - int width = getWidth(); - mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right; - mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left; - } - outline.setRoundRect( - mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f); - } - }); - - // Layout Params for the Icon View - LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); - int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart; - orientationHandler.setIconAppChipChildrenParams( - iconParams, iconMarginStartRelativeToParent); - - mIconView.setLayoutParams(iconParams); - mIconView.setDrawableSize(mAppIconSize, mAppIconSize); - - // Layout Params for the collapsed Icon Text View - int textMarginStart = - iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin; - LayoutParams iconTextCollapsedParams = - (LayoutParams) mIconTextCollapsedView.getLayoutParams(); - orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart); - int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart - - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd; - iconTextCollapsedParams.width = collapsedTextWidth; - mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams); - mIconTextCollapsedView.setAlpha(1f); - - // Layout Params for the expanded Icon Text View - LayoutParams iconTextExpandedParams = - (LayoutParams) mIconTextExpandedView.getLayoutParams(); - orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart); - mIconTextExpandedView.setLayoutParams(iconTextExpandedParams); - mIconTextExpandedView.setAlpha(0f); - mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth); - - // Layout Params for the Icon Arrow View - LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams(); - int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize; - orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart); - mIconArrowView.setPivotY(iconArrowParams.height / 2f); - mIconArrowView.setLayoutParams(iconArrowParams); - - // This method is called twice sometimes (like when rotating split tasks). It is called - // once before onMeasure and onLayout, and again after onMeasure but before onLayout with - // a new width. This happens because we update widths on rotation and on measure of - // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if - // it has just measured, so we explicitly call it here. - measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY)); - } - - @Override - public void setIconColorTint(int color, float amount) { - // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. - float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR); - mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha); - } - - @Override - public void setContentAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha); - } - - @Override - public void setModalAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha); - } - - @Override - public int getDrawableWidth() { - return mIconView == null ? 0 : mIconView.getDrawableWidth(); - } - - @Override - public int getDrawableHeight() { - return mIconView == null ? 0 : mIconView.getDrawableHeight(); - } - - protected void revealAnim(boolean isRevealing) { - cancelInProgressAnimations(); - final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); - final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds(); - final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation); - mAnimator = new AnimatorSet(); - - if (isRevealing) { - boolean isRtl = isLayoutRtl(); - bringToFront(); - // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu - Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal( - mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, - mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth()); - // Animate background clipping - ValueAnimator backgroundAnimator = ValueAnimator.ofObject( - mBackgroundAnimationRectEvaluator, - initialBackground, - expandedBackgroundBounds); - backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); - - float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize; - float arrowTranslationX = - expandedBackgroundBounds.right - collapsedBackgroundBounds.right; - float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin; - float iconCenterToTextExpanded = - mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin; - float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed; - - float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX; - float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX; - - mAnimator.playTogether( - expandedTextRevealAnim, - backgroundAnimator, - ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling), - ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling), - ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, - textTranslationXWithRtl), - ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, - textTranslationXWithRtl), - ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0), - ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1), - ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl), - ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1)); - mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION); - } else { - // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu - Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal( - mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, - mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth()); - - // Animate background clipping - ValueAnimator backgroundAnimator = ValueAnimator.ofObject( - mBackgroundAnimationRectEvaluator, - initialBackground, - collapsedBackgroundBounds); - backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); - - mAnimator.playTogether( - expandedTextClipAnim, - backgroundAnimator, - ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1), - ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1), - ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0), - ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1)); - mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION); - } - - mAnimator.setInterpolator(EMPHASIZED); - mAnimator.start(); - } - - private Rect getCollapsedBackgroundLtrBounds() { - Rect bounds = new Rect( - 0, - 0, - Math.min(mMaxWidth, mCollapsedMenuDefaultWidth), - mCollapsedMenuDefaultHeight); - bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart); - return bounds; - } - - private Rect getExpandedBackgroundLtrBounds() { - return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight); - } - - private void cancelInProgressAnimations() { - // We null the `AnimatorSet` because it holds references to the `Animators` which aren't - // expecting to be mutable and will cause a crash if they are re-used. - if (mAnimator != null && mAnimator.isStarted()) { - mAnimator.cancel(); - mAnimator = null; - } - } - - @Override - public View asView() { - return this; - } -} diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt new file mode 100644 index 000000000..f4fd12792 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt @@ -0,0 +1,464 @@ +/* + * 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.quickstep.views + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.view.ViewAnimationUtils +import android.view.ViewOutlineProvider +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import com.android.app.animation.Interpolators +import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.RecentsOrientedState +import kotlin.math.max +import kotlin.math.min + +/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */ +class IconAppChipView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon { + + private var iconView: IconView? = null + private var iconArrowView: ImageView? = null + private var menuAnchorView: View? = null + // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. + private var iconTextCollapsedView: TextView? = null + private var iconTextExpandedView: TextView? = null + + private val backgroundRelativeLtrLocation = Rect() + private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation) + + // Menu dimensions + private val collapsedMenuDefaultWidth: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width) + private val expandedMenuDefaultWidth: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width) + private val collapsedMenuDefaultHeight = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height) + private val expandedMenuDefaultHeight = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height) + private val iconMenuMarginTopStart = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin) + private val menuToChipGap: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap) + + // Background dimensions + private val backgroundMarginTopStart: Int = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_background_margin_top_start + ) + + // Contents dimensions + private val appNameHorizontalMargin = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed + ) + private val arrowMarginEnd = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin) + private val iconViewMarginStart = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin) + private val appIconSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size) + private val arrowSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size) + private val iconViewDrawableExpandedSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size) + + private var animator: AnimatorSet? = null + + private val multiValueAlpha: MultiValueAlpha = + MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) } + + private val viewTranslationX: MultiPropertyFactory = + MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) + + private val viewTranslationY: MultiPropertyFactory = + MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) + + var maxWidth = Int.MAX_VALUE + /** + * Sets the maximum width of this Icon Menu. This is usually used when space is limited for + * split screen. + */ + set(value) { + // Width showing only the app icon and arrow. Max width should not be set to less than + // this. + val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd + field = max(value, minMaxWidth) + } + + var status: AppChipStatus = AppChipStatus.Collapsed + private set + + override fun onFinishInflate() { + super.onFinishInflate() + iconView = findViewById(R.id.icon_view) + iconTextCollapsedView = findViewById(R.id.icon_text_collapsed) + iconTextExpandedView = findViewById(R.id.icon_text_expanded) + iconArrowView = findViewById(R.id.icon_arrow) + menuAnchorView = findViewById(R.id.icon_view_menu_anchor) + } + + override fun setText(text: CharSequence?) { + iconTextCollapsedView?.text = text + iconTextExpandedView?.text = text + } + + override fun getDrawable(): Drawable? = iconView?.drawable + + override fun setDrawable(icon: Drawable?) { + iconView?.drawable = icon + } + + override fun setDrawableSize(iconWidth: Int, iconHeight: Int) { + iconView?.setDrawableSize(iconWidth, iconHeight) + } + + override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) { + val orientationHandler = orientationState.orientationHandler + // Layout params for anchor view + val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams + anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap + menuAnchorView!!.layoutParams = anchorLayoutParams + + // Layout Params for the Menu View (this) + val iconMenuParams = layoutParams as LayoutParams + iconMenuParams.width = expandedMenuDefaultWidth + iconMenuParams.height = expandedMenuDefaultHeight + orientationHandler.setIconAppChipMenuParams( + this, + iconMenuParams, + iconMenuMarginTopStart, + iconMenuMarginTopStart, + ) + layoutParams = iconMenuParams + + // Layout params for the background + val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() + backgroundRelativeLtrLocation.set(collapsedBackgroundBounds) + outlineProvider = + object : ViewOutlineProvider() { + val mRtlAppliedOutlineBounds: Rect = Rect() + + override fun getOutline(view: View, outline: Outline) { + mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation) + if (isLayoutRtl) { + val width = width + mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right + mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left + } + outline.setRoundRect( + mRtlAppliedOutlineBounds, + mRtlAppliedOutlineBounds.height() / 2f, + ) + } + } + + // Layout Params for the Icon View + val iconParams = iconView!!.layoutParams as LayoutParams + val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart + orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent) + + iconView!!.layoutParams = iconParams + iconView!!.setDrawableSize(appIconSize, appIconSize) + + // Layout Params for the collapsed Icon Text View + val textMarginStart = + iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin + val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams + orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart) + val collapsedTextWidth = + (collapsedBackgroundBounds.width() - + iconViewMarginStart - + appIconSize - + arrowSize - + appNameHorizontalMargin - + arrowMarginEnd) + iconTextCollapsedParams.width = collapsedTextWidth + iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams + iconTextCollapsedView!!.alpha = 1f + + // Layout Params for the expanded Icon Text View + val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams + orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart) + iconTextExpandedView!!.layoutParams = iconTextExpandedParams + iconTextExpandedView!!.alpha = 0f + iconTextExpandedView!!.setRevealClip( + true, + 0f, + appIconSize / 2f, + collapsedTextWidth.toFloat(), + ) + + // Layout Params for the Icon Arrow View + val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams + val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize + orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart) + iconArrowView!!.pivotY = iconArrowParams.height / 2f + iconArrowView!!.layoutParams = iconArrowParams + + // This method is called twice sometimes (like when rotating split tasks). It is called + // once before onMeasure and onLayout, and again after onMeasure but before onLayout with + // a new width. This happens because we update widths on rotation and on measure of + // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if + // it has just measured, so we explicitly call it here. + measure( + MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY), + ) + } + + override fun setIconColorTint(color: Int, amount: Float) { + // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. + val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR) + multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha + } + + override fun setContentAlpha(alpha: Float) { + multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha + } + + override fun setModalAlpha(alpha: Float) { + multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha + } + + override fun setFlexSplitAlpha(alpha: Float) { + multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha + } + + override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0 + + override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0 + + /** Gets the view split x-axis translation */ + fun getSplitTranslationX(): MultiPropertyFactory.MultiProperty = + viewTranslationX.get(INDEX_SPLIT_TRANSLATION) + + /** + * Sets the view split x-axis translation + * + * @param value x-axis translation + */ + fun setSplitTranslationX(value: Float) { + getSplitTranslationX().value = value + } + + /** Gets the view split y-axis translation */ + fun getSplitTranslationY(): MultiPropertyFactory.MultiProperty = + viewTranslationY[INDEX_SPLIT_TRANSLATION] + + /** + * Sets the view split y-axis translation + * + * @param value y-axis translation + */ + fun setSplitTranslationY(value: Float) { + getSplitTranslationY().value = value + } + + /** Gets the menu x-axis translation for split task */ + fun getMenuTranslationX(): MultiPropertyFactory.MultiProperty = + viewTranslationX[INDEX_MENU_TRANSLATION] + + /** Gets the menu y-axis translation for split task */ + fun getMenuTranslationY(): MultiPropertyFactory.MultiProperty = + viewTranslationY[INDEX_MENU_TRANSLATION] + + internal fun revealAnim(isRevealing: Boolean, animated: Boolean = true) { + cancelInProgressAnimations() + val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() + val expandedBackgroundBounds = getExpandedBackgroundLtrBounds() + val initialBackground = Rect(backgroundRelativeLtrLocation) + animator = AnimatorSet() + + if (isRevealing) { + val isRtl = isLayoutRtl + bringToFront() + // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu + val expandedTextRevealAnim = + ViewAnimationUtils.createCircularReveal( + iconTextExpandedView, + 0, + iconTextExpandedView!!.height / 2, + iconTextCollapsedView!!.width.toFloat(), + iconTextExpandedView!!.width.toFloat(), + ) + // Animate background clipping + val backgroundAnimator = + ValueAnimator.ofObject( + backgroundAnimationRectEvaluator, + initialBackground, + expandedBackgroundBounds, + ) + backgroundAnimator.addUpdateListener { invalidateOutline() } + + val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat() + val arrowTranslationX = + (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat() + val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin + val iconCenterToTextExpanded = + iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin + val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed + + val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX + val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX + + animator!!.playTogether( + expandedTextRevealAnim, + backgroundAnimator, + ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling), + ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling), + ObjectAnimator.ofFloat( + iconTextCollapsedView, + TRANSLATION_X, + textTranslationXWithRtl, + ), + ObjectAnimator.ofFloat( + iconTextExpandedView, + TRANSLATION_X, + textTranslationXWithRtl, + ), + ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f), + ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f), + ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl), + ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f), + ) + animator!!.duration = MENU_BACKGROUND_REVEAL_DURATION.toLong() + status = AppChipStatus.Expanded + } else { + // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu + val expandedTextClipAnim = + ViewAnimationUtils.createCircularReveal( + iconTextExpandedView, + 0, + iconTextExpandedView!!.height / 2, + iconTextExpandedView!!.width.toFloat(), + iconTextCollapsedView!!.width.toFloat(), + ) + + // Animate background clipping + val backgroundAnimator = + ValueAnimator.ofObject( + backgroundAnimationRectEvaluator, + initialBackground, + collapsedBackgroundBounds, + ) + backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? -> + invalidateOutline() + } + + animator!!.playTogether( + expandedTextClipAnim, + backgroundAnimator, + ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f), + ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f), + ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f), + ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f), + ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f), + ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f), + ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f), + ) + animator!!.duration = MENU_BACKGROUND_HIDE_DURATION.toLong() + status = AppChipStatus.Collapsed + } + + if (!animated) animator!!.duration = 0 + animator!!.interpolator = Interpolators.EMPHASIZED + animator!!.start() + } + + private fun getCollapsedBackgroundLtrBounds(): Rect { + val bounds = + Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight) + bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart) + return bounds + } + + private fun getExpandedBackgroundLtrBounds() = + Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight) + + private fun cancelInProgressAnimations() { + // We null the `AnimatorSet` because it holds references to the `Animators` which aren't + // expecting to be mutable and will cause a crash if they are re-used. + if (animator != null && animator!!.isStarted) { + animator!!.cancel() + animator = null + } + } + + override fun focusSearch(direction: Int): View? { + if (mParent == null) return null + return when (direction) { + FOCUS_RIGHT, + FOCUS_DOWN -> mParent.focusSearch(this, View.FOCUS_FORWARD) + FOCUS_UP, + FOCUS_LEFT -> mParent.focusSearch(this, View.FOCUS_BACKWARD) + else -> super.focusSearch(direction) + } + } + + fun reset() { + setText(null) + setDrawable(null) + } + + override fun asView(): View = this + + enum class AppChipStatus { + Expanded, + Collapsed, + } + + private companion object { + private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b } + + private const val MENU_BACKGROUND_REVEAL_DURATION = 417 + private const val MENU_BACKGROUND_HIDE_DURATION = 333 + + private const val NUM_ALPHA_CHANNELS = 4 + private const val INDEX_CONTENT_ALPHA = 0 + private const val INDEX_COLOR_FILTER_ALPHA = 1 + private const val INDEX_MODAL_ALPHA = 2 + /** Used to hide the app chip for 90:10 flex split. */ + private const val INDEX_MINIMUM_RATIO_ALPHA = 3 + + private const val INDEX_SPLIT_TRANSLATION = 0 + private const val INDEX_MENU_TRANSLATION = 1 + private const val INDEX_COUNT_TRANSLATION = 2 + } +} diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java deleted file mode 100644 index bb4a7ecda..000000000 --- a/quickstep/src/com/android/quickstep/views/IconView.java +++ /dev/null @@ -1,209 +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.quickstep.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.Nullable; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.launcher3.views.ActivityContext; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.RecentsOrientedState; - -/** - * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout - * when the drawable changes. - */ -public class IconView extends View implements TaskViewIcon { - private static final int NUM_ALPHA_CHANNELS = 2; - private static final int INDEX_CONTENT_ALPHA = 0; - private static final int INDEX_MODAL_ALPHA = 1; - - private final MultiValueAlpha mMultiValueAlpha; - - @Nullable - private Drawable mDrawable; - private int mDrawableWidth, mDrawableHeight; - - public IconView(Context context) { - this(context, null); - } - - public IconView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public IconView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS); - mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true); - } - - /** - * Sets a {@link Drawable} to be displayed. - */ - @Override - public void setDrawable(@Nullable Drawable d) { - if (mDrawable != null) { - mDrawable.setCallback(null); - } - mDrawable = d; - if (mDrawable != null) { - mDrawable.setCallback(this); - setDrawableSizeInternal(getWidth(), getHeight()); - } - invalidate(); - } - - /** - * Sets the size of the icon drawable. - */ - @Override - public void setDrawableSize(int iconWidth, int iconHeight) { - mDrawableWidth = iconWidth; - mDrawableHeight = iconHeight; - if (mDrawable != null) { - setDrawableSizeInternal(getWidth(), getHeight()); - } - } - - private void setDrawableSizeInternal(int selfWidth, int selfHeight) { - Rect selfRect = new Rect(0, 0, selfWidth, selfHeight); - Rect drawableRect = new Rect(); - Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect); - mDrawable.setBounds(drawableRect); - } - - @Override - @Nullable - public Drawable getDrawable() { - return mDrawable; - } - - @Override - public int getDrawableWidth() { - return mDrawableWidth; - } - - @Override - public int getDrawableHeight() { - return mDrawableHeight; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (mDrawable != null) { - setDrawableSizeInternal(w, h); - } - } - - @Override - protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || who == mDrawable; - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - - final Drawable drawable = mDrawable; - if (drawable != null && drawable.isStateful() - && drawable.setState(getDrawableState())) { - invalidateDrawable(drawable); - } - } - - @Override - protected void onDraw(Canvas canvas) { - if (mDrawable != null) { - mDrawable.draw(canvas); - } - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - @Override - public void setContentAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha); - } - - @Override - public void setModalAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha); - } - - /** - * Set the tint color of the icon, useful for scrimming or dimming. - * - * @param color to blend in. - * @param amount [0,1] 0 no tint, 1 full tint - */ - @Override - public void setIconColorTint(int color, float amount) { - if (mDrawable != null) { - mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount)); - } - } - - @Override - public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) { - RecentsPagedOrientationHandler orientationHandler = - orientationState.getOrientationHandler(); - boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; - DeviceProfile deviceProfile = - ActivityContext.lookupContext(getContext()).getDeviceProfile(); - - FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams(); - - int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; - int taskIconHeight = deviceProfile.overviewTaskIconSizePx; - int taskMargin = deviceProfile.overviewTaskMarginPx; - - orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight, - thumbnailTopMargin, isRtl); - iconParams.width = iconParams.height = taskIconHeight; - setLayoutParams(iconParams); - - setRotation(orientationHandler.getDegreesRotated()); - int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx - : deviceProfile.overviewTaskIconDrawableSizePx; - setDrawableSize(iconDrawableSize, iconDrawableSize); - } - - @Override - public View asView() { - return this; - } -} diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt new file mode 100644 index 000000000..cb69b22de --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconView.kt @@ -0,0 +1,195 @@ +/* + * 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.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.updateLayoutParams +import com.android.launcher3.DeviceProfile +import com.android.launcher3.Flags +import com.android.launcher3.Utilities +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.launcher3.util.MultiValueAlpha +import com.android.launcher3.views.ActivityContext +import com.android.quickstep.util.RecentsOrientedState +import com.google.android.msdl.data.model.MSDLToken + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +class IconView : View, TaskViewIcon { + private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS) + private var drawable: Drawable? = null + private var drawableWidth = 0 + private var drawableHeight = 0 + private var msdlPlayerWrapper: MSDLPlayerWrapper? = null + + constructor(context: Context) : super(context) { + setUpHaptics() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + setUpHaptics() + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) { + setUpHaptics() + } + + init { + multiValueAlpha.setUpdateVisibility(true) + } + + private fun setUpHaptics() { + msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context) + // Haptics are handled by the MSDLPlayerWrapper + isHapticFeedbackEnabled = !Flags.msdlFeedback() || msdlPlayerWrapper == null + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + super.setOnLongClickListener(l?.withFeedback()) + } + + /** Sets a [Drawable] to be displayed. */ + override fun setDrawable(d: Drawable?) { + drawable?.callback = null + + // Copy drawable so that mutations below do not affect other users of the drawable + drawable = d?.constantState?.newDrawable()?.mutate() + drawable?.let { + it.callback = this + setDrawableSizeInternal(width, height) + } + invalidate() + } + + /** Sets the size of the icon drawable. */ + override fun setDrawableSize(iconWidth: Int, iconHeight: Int) { + drawableWidth = iconWidth + drawableHeight = iconHeight + drawable?.let { setDrawableSizeInternal(width, height) } + } + + private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) { + val selfRect = Rect(0, 0, selfWidth, selfHeight) + val drawableRect = Rect() + Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect) + drawable?.bounds = drawableRect + } + + override fun getDrawable(): Drawable? = drawable + + override fun getDrawableWidth(): Int = drawableWidth + + override fun getDrawableHeight(): Int = drawableHeight + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + drawable?.let { setDrawableSizeInternal(w, h) } + } + + override fun verifyDrawable(who: Drawable): Boolean = + super.verifyDrawable(who) || who === drawable + + override fun drawableStateChanged() { + super.drawableStateChanged() + drawable?.let { + if (it.isStateful && it.setState(drawableState)) { + invalidateDrawable(it) + } + } + } + + override fun onDraw(canvas: Canvas) { + drawable?.draw(canvas) + } + + override fun hasOverlappingRendering(): Boolean = false + + override fun setContentAlpha(alpha: Float) { + multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha) + } + + override fun setModalAlpha(alpha: Float) { + multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha) + } + + override fun setFlexSplitAlpha(alpha: Float) { + multiValueAlpha[INDEX_FLEX_SPLIT_ALPHA].setValue(alpha) + } + + /** + * Set the tint color of the icon, useful for scrimming or dimming. + * + * @param color to blend in. + * @param amount [0,1] 0 no tint, 1 full tint + */ + override fun setIconColorTint(color: Int, amount: Float) { + drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount) + } + + override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) { + val orientationHandler = orientationState.orientationHandler + val deviceProfile: DeviceProfile = + (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile() + orientationHandler.setTaskIconParams( + iconParams = getLayoutParams() as FrameLayout.LayoutParams, + taskIconMargin = deviceProfile.overviewTaskMarginPx, + taskIconHeight = deviceProfile.overviewTaskIconSizePx, + thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx, + isRtl = layoutDirection == LAYOUT_DIRECTION_RTL, + ) + updateLayoutParams { + height = deviceProfile.overviewTaskIconSizePx + width = height + } + setRotation(orientationHandler.degreesRotated) + val iconDrawableSize = + if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx + else deviceProfile.overviewTaskIconDrawableSizePx + setDrawableSize(iconDrawableSize, iconDrawableSize) + } + + override fun asView(): View = this + + private fun OnLongClickListener.withFeedback(): OnLongClickListener { + val delegate = this + return OnLongClickListener { v: View -> + if (Flags.msdlFeedback()) { + msdlPlayerWrapper?.playToken(MSDLToken.LONG_PRESS) + } + delegate.onLongClick(v) + } + } + + companion object { + private const val NUM_ALPHA_CHANNELS = 3 + private const val INDEX_CONTENT_ALPHA = 0 + private const val INDEX_MODAL_ALPHA = 1 + private const val INDEX_FLEX_SPLIT_ALPHA = 2 + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index e48a7c605..b86d0c85d 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -16,17 +16,16 @@ package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; -import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON; -import static com.android.launcher3.LauncherState.EDIT_MODE; +import static com.android.launcher3.LauncherState.ADD_DESK_BUTTON; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; -import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; -import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity; import android.annotation.TargetApi; import android.content.Context; @@ -39,7 +38,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.desktop.DesktopRecentsTransitionController; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.statehandlers.DepthController; @@ -50,12 +48,13 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; +import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.GestureState; import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.SplitSelectStateController; -import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedTaskInfo; import kotlin.Unit; @@ -75,7 +74,7 @@ public class LauncherRecentsView extends RecentsView - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)); + mBlurUtils.setDrawLiveTileBelowRecents(true); } } @@ -199,7 +210,10 @@ public class LauncherRecentsView extends RecentsView getContainerInterface(int displayId) { + return LauncherActivityInterface.INSTANCE; + } + @Override protected void onDismissAnimationEnds() { super.onDismissAnimationEnds(); @@ -237,10 +256,10 @@ public class LauncherRecentsView extends RecentsView extends FrameLayo // Currently, the only grouped task action is "save app pairs". mActionButtons = findViewById(R.id.action_buttons); mSaveAppPairButton = findViewById(R.id.action_save_app_pair); + TypefaceUtils.setTypeface(mSaveAppPairButton, FontFamily.GSF_LABEL_LARGE); // Initialize a list to hold alphas for mActionButtons and any group action buttons. mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS); mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] = @@ -274,7 +277,9 @@ public class OverviewActionsView extends FrameLayo } private void updateActionButtonsVisibility() { - assert mDp != null; + if (mDp == null) { + return; + } boolean showSingleTaskActions = !mIsGroupedTask; boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair; Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = [" diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt new file mode 100644 index 000000000..6acaeae8d --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.os.VibrationAttributes +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.FloatValueHolder +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.Flags.enableGridOnlyOverview +import com.android.launcher3.R +import com.android.launcher3.Utilities.boundToRange +import com.android.launcher3.util.DynamicResource +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.quickstep.util.TaskGridNavHelper +import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.InteractionProperties +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.math.sign + +/** + * Helper class for [RecentsView]. This util class contains refactored and extracted functions from + * RecentsView related to TaskView dismissal. + */ +class RecentsDismissUtils(private val recentsView: RecentsView<*, *>) { + + /** + * Creates the spring animations which run when a dragged task view in overview is released. + * + *

When a task dismiss is cancelled, the task will return to its original position via a + * spring animation. As it passes the threshold of its settling state, its neighbors will spring + * in response to the perceived impact of the settling task. + */ + fun createTaskDismissSettlingSpringAnimation( + draggedTaskView: TaskView?, + velocity: Float, + isDismissing: Boolean, + dismissLength: Int, + onEndRunnable: () -> Unit, + ): SpringAnimation? { + draggedTaskView ?: return null + val taskDismissFloatProperty = + FloatPropertyCompat.createFloatPropertyCompat( + draggedTaskView.secondaryDismissTranslationProperty + ) + val minVelocity = + recentsView.pagedOrientationHandler.getSecondaryDimension(draggedTaskView).toFloat() + val startVelocity = abs(velocity).coerceAtLeast(minVelocity) * velocity.sign + // Animate dragged task towards dismissal or rest state. + val draggedTaskViewSpringAnimation = + SpringAnimation(draggedTaskView, taskDismissFloatProperty) + .setSpring(createExpressiveDismissSpringForce()) + .setStartVelocity(startVelocity) + .addUpdateListener { animation, value, _ -> + if (isDismissing && abs(value) >= abs(dismissLength)) { + animation.cancel() + } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) { + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value = + taskDismissFloatProperty.getValue(draggedTaskView) + } + recentsView.redrawLiveTile() + } + } + .addEndListener { _, _, _, _ -> + if (isDismissing) { + if (!recentsView.showAsGrid() || enableGridOnlyOverview()) { + runTaskGridReflowSpringAnimation( + draggedTaskView, + getDismissedTaskGapForReflow(draggedTaskView), + onEndRunnable, + ) + } else { + recentsView.dismissTaskView( + draggedTaskView, + /* animateTaskView = */ false, + /* removeTask = */ true, + ) + onEndRunnable() + } + } else { + recentsView.onDismissAnimationEnds() + onEndRunnable() + } + } + if (!isDismissing) { + addNeighborSettlingSpringAnimations( + draggedTaskView, + draggedTaskViewSpringAnimation, + driverProgressThreshold = 0f, + isSpringDirectionVertical = true, + minVelocity = startVelocity, + ) + } + return draggedTaskViewSpringAnimation + } + + private fun addNeighborSettlingSpringAnimations( + draggedTaskView: TaskView, + springAnimationDriver: SpringAnimation, + tasksToExclude: List = emptyList(), + driverProgressThreshold: Float, + isSpringDirectionVertical: Boolean, + minVelocity: Float, + ) { + // Empty spring animation exists for conditional start, and to drive neighboring springs. + val neighborsToSettle = + SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce()) + + // Add tasks before dragged index, fanning out from the dragged task. + // The order they are added matters, as each spring drives the next. + var previousNeighbor = neighborsToSettle + getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true) + .filter { (taskView, _) -> !tasksToExclude.contains(taskView) } + .forEach { (taskView, offset) -> + previousNeighbor = + createNeighboringTaskViewSpringAnimation( + taskView, + offset * ADDITIONAL_DISMISS_DAMPING_RATIO, + previousNeighbor, + isSpringDirectionVertical, + ) + } + // Add tasks after dragged index, fanning out from the dragged task. + // The order they are added matters, as each spring drives the next. + previousNeighbor = neighborsToSettle + getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false) + .filter { (taskView, _) -> !tasksToExclude.contains(taskView) } + .forEach { (taskView, offset) -> + previousNeighbor = + createNeighboringTaskViewSpringAnimation( + taskView, + offset * ADDITIONAL_DISMISS_DAMPING_RATIO, + previousNeighbor, + isSpringDirectionVertical, + ) + } + + val isCurrentDisplacementAboveOrigin = + recentsView.pagedOrientationHandler.isGoingUp( + draggedTaskView.secondaryDismissTranslationProperty.get(draggedTaskView), + recentsView.isRtl, + ) + addThresholdSpringAnimationTrigger( + springAnimationDriver, + progressThreshold = driverProgressThreshold, + neighborsToSettle, + isCurrentDisplacementAboveOrigin, + minVelocity, + ) + } + + /** As spring passes threshold for the first time, run conditional spring with velocity. */ + private fun addThresholdSpringAnimationTrigger( + springAnimationDriver: SpringAnimation, + progressThreshold: Float, + conditionalSpring: SpringAnimation, + isCurrentDisplacementAboveOrigin: Boolean, + minVelocity: Float, + ) { + val runSettlingAtVelocity = { velocity: Float -> + conditionalSpring.setStartVelocity(velocity).animateToFinalPosition(0f) + playDismissSettlingHaptic(velocity) + } + if (isCurrentDisplacementAboveOrigin) { + var lastPosition = 0f + var startSettling = false + springAnimationDriver.addUpdateListener { _, value, velocity -> + // We do not compare to the threshold directly, as the update listener + // does not necessarily hit every value. Do not check again once it has started + // settling, as a spring can bounce past the end value multiple times. + if (startSettling) return@addUpdateListener + if ( + lastPosition < progressThreshold && value >= progressThreshold || + lastPosition > progressThreshold && value <= progressThreshold + ) { + startSettling = true + } + lastPosition = value + if (startSettling) { + runSettlingAtVelocity(velocity) + } + } + } else { + // Run settling animations immediately when displacement is already below settled state. + runSettlingAtVelocity(minVelocity) + } + } + + /** + * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order. + * + *

Gets tasks either before or after the dragged task along with their offset from it. The + * offset is the distance between indices for carousels, or distance between columns for grids. + */ + private fun getTasksOffsetPairAdjacentToDraggedTask( + draggedTaskView: TaskView, + towardsStart: Boolean, + ): Sequence> { + if (recentsView.showAsGrid()) { + val taskGridNavHelper = + TaskGridNavHelper( + recentsView.mUtils.getTopRowIdArray(), + recentsView.mUtils.getBottomRowIdArray(), + recentsView.mUtils.getLargeTaskViewIds(), + hasAddDesktopButton = false, + ) + return taskGridNavHelper + .gridTaskViewIdOffsetPairInTabOrderSequence( + draggedTaskView.taskViewId, + towardsStart, + ) + .mapNotNull { (taskViewId, columnOffset) -> + recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView -> + Pair(taskView, columnOffset) + } + } + } else { + val taskViewList = recentsView.mUtils.taskViews.toList() + val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView) + + return if (towardsStart) { + taskViewList + .take(draggedTaskViewIndex) + .reversed() + .mapIndexed { index, taskView -> Pair(taskView, index + 1) } + .asSequence() + } else { + taskViewList + .takeLast(taskViewList.size - draggedTaskViewIndex - 1) + .mapIndexed { index, taskView -> Pair(taskView, index + 1) } + .asSequence() + } + } + } + + /** Creates a neighboring task view spring, driven by the spring of its neighbor. */ + private fun createNeighboringTaskViewSpringAnimation( + taskView: TaskView, + dampingOffsetRatio: Float, + previousNeighborSpringAnimation: SpringAnimation, + springingDirectionVertical: Boolean, + ): SpringAnimation { + val springProperty = + if (springingDirectionVertical) taskView.secondaryDismissTranslationProperty + else taskView.primaryDismissTranslationProperty + val neighboringTaskViewSpringAnimation = + SpringAnimation(taskView, FloatPropertyCompat.createFloatPropertyCompat(springProperty)) + .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio)) + // Update live tile on spring animation. + if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) { + neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ -> + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + val taskTranslation = + if (springingDirectionVertical) { + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation + } else { + remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation + } + taskTranslation.value = springProperty.get(taskView) + } + recentsView.redrawLiveTile() + } + } + // Drive current neighbor's spring with the previous neighbor's. + previousNeighborSpringAnimation.addUpdateListener { _, value, _ -> + neighboringTaskViewSpringAnimation.animateToFinalPosition(value) + } + return neighboringTaskViewSpringAnimation + } + + private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + return SpringForce() + .setDampingRatio( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) + + dampingRatioOffset + ) + .setStiffness( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness) + ) + } + + private fun createExpressiveGridReflowSpringForce( + finalPosition: Float = Float.MAX_VALUE + ): SpringForce { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + return SpringForce(finalPosition) + .setDampingRatio( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_damping_ratio) + ) + .setStiffness( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_stiffness) + ) + } + + /** + * Plays a haptic as the dragged task view settles back into its rest state. + * + *

Haptic intensity is proportional to velocity. + */ + private fun playDismissSettlingHaptic(velocity: Float) { + val maxDismissSettlingVelocity = + recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView) + MSDLPlayerWrapper.INSTANCE.get(recentsView.context) + ?.playToken( + MSDLToken.CANCEL, + InteractionProperties.DynamicVibrationScale( + boundToRange(abs(velocity) / maxDismissSettlingVelocity, 0f, 1f), + VibrationAttributes.Builder() + .setUsage(VibrationAttributes.USAGE_TOUCH) + .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT) + .build(), + ), + ) + } + + /** Animates RecentsView's scale to the provided value, using spring animations. */ + fun animateRecentsScale(scale: Float): SpringAnimation { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio) + val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness) + + // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation + // struggles to animate small values like changing recents scale from 0.9 to 1. So + // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value. + // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and + // animating it directly via springs.) + val initialRecentsScaleSpringValue = + RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView) + return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue)) + .setSpring( + SpringForce(initialRecentsScaleSpringValue) + .setDampingRatio(dampingRatio) + .setStiffness(stiffness) + ) + .addUpdateListener { _, value, _ -> + RECENTS_SCALE_PROPERTY.setValue( + recentsView, + value / RECENTS_SCALE_SPRING_MULTIPLIER, + ) + } + .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) } + } + + /** Animates with springs the TaskViews beyond the dismissed task to fill the gap it left. */ + private fun runTaskGridReflowSpringAnimation( + dismissedTaskView: TaskView, + dismissedTaskGap: Float, + onEndRunnable: () -> Unit, + ) { + // Empty spring animation exists for conditional start, and to drive neighboring springs. + val springAnimationDriver = + SpringAnimation(FloatValueHolder()) + .setSpring(createExpressiveGridReflowSpringForce(finalPosition = dismissedTaskGap)) + val towardsStart = if (recentsView.isRtl) dismissedTaskGap < 0 else dismissedTaskGap > 0 + + var tasksToReflow: List + // Build the chains of Spring Animations + when { + !recentsView.showAsGrid() -> { + tasksToReflow = + getTasksToReflow( + recentsView.mUtils.taskViews.toList(), + dismissedTaskView, + towardsStart, + ) + buildDismissReflowSpringAnimationChain( + tasksToReflow, + dismissedTaskGap, + previousSpring = springAnimationDriver, + ) + } + dismissedTaskView.isLargeTile -> { + tasksToReflow = + getTasksToReflow( + recentsView.mUtils.getLargeTaskViews(), + dismissedTaskView, + towardsStart, + ) + val lastSpringAnimation = + buildDismissReflowSpringAnimationChain( + tasksToReflow, + dismissedTaskGap, + previousSpring = springAnimationDriver, + ) + // Add all top and bottom grid tasks when animating towards the end of the grid. + if (!towardsStart) { + tasksToReflow += recentsView.mUtils.getTopRowTaskViews() + tasksToReflow += recentsView.mUtils.getBottomRowTaskViews() + buildDismissReflowSpringAnimationChain( + recentsView.mUtils.getTopRowTaskViews(), + dismissedTaskGap, + previousSpring = lastSpringAnimation, + ) + buildDismissReflowSpringAnimationChain( + recentsView.mUtils.getBottomRowTaskViews(), + dismissedTaskGap, + previousSpring = lastSpringAnimation, + ) + } + } + recentsView.isOnGridBottomRow(dismissedTaskView) -> { + tasksToReflow = + getTasksToReflow( + recentsView.mUtils.getBottomRowTaskViews(), + dismissedTaskView, + towardsStart, + ) + buildDismissReflowSpringAnimationChain( + tasksToReflow, + dismissedTaskGap, + previousSpring = springAnimationDriver, + ) + } + else -> { + tasksToReflow = + getTasksToReflow( + recentsView.mUtils.getTopRowTaskViews(), + dismissedTaskView, + towardsStart, + ) + buildDismissReflowSpringAnimationChain( + tasksToReflow, + dismissedTaskGap, + previousSpring = springAnimationDriver, + ) + } + } + + if (tasksToReflow.isNotEmpty()) { + addNeighborSettlingSpringAnimations( + dismissedTaskView, + springAnimationDriver, + tasksToExclude = tasksToReflow, + driverProgressThreshold = dismissedTaskGap, + isSpringDirectionVertical = false, + minVelocity = 0f, + ) + } else { + springAnimationDriver.addEndListener { _, _, _, _ -> + // Play the same haptic as when neighbors spring into place. + MSDLPlayerWrapper.INSTANCE.get(recentsView.context)?.playToken(MSDLToken.CANCEL) + } + } + + // Start animations and remove the dismissed task at the end, dismiss immediately if no + // neighboring tasks exist. + val runGridEndAnimationAndRelayout = { + recentsView.expressiveDismissTaskView(dismissedTaskView, onEndRunnable) + } + springAnimationDriver?.apply { + addEndListener { _, _, _, _ -> runGridEndAnimationAndRelayout() } + animateToFinalPosition(dismissedTaskGap) + } ?: runGridEndAnimationAndRelayout() + } + + private fun getDismissedTaskGapForReflow(dismissedTaskView: TaskView): Float { + val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView) + val screenEnd = + screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView) + val taskStart = + recentsView.pagedOrientationHandler.getChildStart(dismissedTaskView) + + dismissedTaskView.getOffsetAdjustment(recentsView.showAsGrid()) + val taskSize = + recentsView.pagedOrientationHandler.getMeasuredSize(dismissedTaskView) * + dismissedTaskView.getSizeAdjustment(recentsView.showAsFullscreen()) + val taskEnd = taskStart + taskSize + + val isDismissedTaskBeyondEndOfScreen = + if (recentsView.isRtl) taskEnd > screenEnd else taskStart < screenStart + if ( + dismissedTaskView.isLargeTile && + isDismissedTaskBeyondEndOfScreen && + recentsView.mUtils.getLargeTileCount() == 1 + ) { + return with(recentsView) { + pagedOrientationHandler.getPrimaryScroll(this) - + getScrollForPage(indexOfChild(mUtils.getFirstNonDesktopTaskView())) + } + .toFloat() + } + + // If current page is beyond last TaskView's index, use last TaskView to calculate offset. + val lastTaskViewIndex = recentsView.indexOfChild(recentsView.mUtils.getLastTaskView()) + val currentPage = recentsView.currentPage.coerceAtMost(lastTaskViewIndex) + val dismissHorizontalFactor = + when { + dismissedTaskView.isGridTask -> 1f + currentPage == lastTaskViewIndex -> -1f + recentsView.indexOfChild(dismissedTaskView) < currentPage -> -1f + else -> 1f + } * (if (recentsView.isRtl) 1f else -1f) + + return (recentsView.pagedOrientationHandler.getPrimarySize(dismissedTaskView) + + recentsView.pageSpacing) * dismissHorizontalFactor + } + + private fun getTasksToReflow( + taskViews: List, + dismissedTaskView: TaskView, + towardsStart: Boolean, + ): List { + val dismissedTaskViewIndex = taskViews.indexOf(dismissedTaskView) + if (dismissedTaskViewIndex == -1) { + return emptyList() + } + return if (towardsStart) { + taskViews.take(dismissedTaskViewIndex).reversed() + } else { + taskViews.takeLast(taskViews.size - dismissedTaskViewIndex - 1) + } + } + + private fun willTaskBeVisibleAfterDismiss(taskView: TaskView, taskTranslation: Int): Boolean { + val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView) + val screenEnd = + screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView) + return recentsView.isTaskViewWithinBounds( + taskView, + screenStart, + screenEnd, + /* taskViewTranslation = */ taskTranslation, + ) + } + + /** Builds a chain of spring animations for task reflow after dismissal */ + private fun buildDismissReflowSpringAnimationChain( + taskViews: Iterable, + dismissedTaskGap: Float, + previousSpring: SpringAnimation, + ): SpringAnimation { + var lastTaskViewSpring = previousSpring + taskViews + .filter { taskView -> + willTaskBeVisibleAfterDismiss(taskView, dismissedTaskGap.roundToInt()) + } + .forEach { taskView -> + val taskViewSpringAnimation = + SpringAnimation( + taskView, + FloatPropertyCompat.createFloatPropertyCompat( + taskView.primaryDismissTranslationProperty + ), + ) + .setSpring(createExpressiveGridReflowSpringForce(dismissedTaskGap)) + // Update live tile on spring animation. + if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) { + taskViewSpringAnimation.addUpdateListener { _, _, _ -> + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation.value = + taskView.primaryDismissTranslationProperty.get(taskView) + } + recentsView.redrawLiveTile() + } + } + lastTaskViewSpring.addUpdateListener { _, value, _ -> + taskViewSpringAnimation.animateToFinalPosition(value) + } + lastTaskViewSpring = taskViewSpringAnimation + } + return lastTaskViewSpring + } + + private companion object { + // The additional damping to apply to tasks further from the dismissed task. + private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f + private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index d7c785765..3e4b953b5 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -17,6 +17,8 @@ package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.os.Trace.traceBegin; +import static android.os.Trace.traceEnd; import static android.view.Surface.ROTATION_0; import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.makeMeasureSpec; @@ -25,21 +27,25 @@ import static com.android.app.animation.Interpolators.ACCELERATE; import static com.android.app.animation.Interpolators.ACCELERATE_0_75; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; import static com.android.app.animation.Interpolators.DECELERATE_2; +import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.app.animation.Interpolators.OVERSHOOT_0_75; import static com.android.app.animation.Interpolators.clampToProgress; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; -import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; -import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; +import static com.android.launcher3.Flags.enableDesktopExplodedView; +import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation; import static com.android.launcher3.Flags.enableGridOnlyOverview; +import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile; +import static com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur; import static com.android.launcher3.Flags.enableRefactorTaskThumbnail; +import static com.android.launcher3.Flags.enableSeparateExternalDisplayTasks; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; @@ -51,27 +57,26 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; +import static com.android.launcher3.statehandlers.DesktopVisibilityController.INACTIVE_DESK_ID; import static com.android.launcher3.testing.shared.TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE; import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; +import static com.android.quickstep.BaseContainerInterface.getTaskDimension; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; +import static com.android.quickstep.util.DesksUtils.areMultiDesksFlagsEnabled; import static com.android.quickstep.util.LogUtils.splitFailureMessage; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP; import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS; -import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE; +import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS; +import static com.android.quickstep.views.TaskView.SPLIT_ALPHA; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -82,7 +87,6 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.annotation.SuppressLint; -import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; import android.content.LocusId; @@ -90,6 +94,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; @@ -99,6 +104,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.SystemClock; +import android.os.Trace; import android.os.UserHandle; import android.os.VibrationEffect; import android.text.Layout; @@ -124,12 +130,15 @@ import android.view.animation.Interpolator; import android.widget.ListView; import android.widget.OverScroller; import android.widget.Toast; +import android.window.DesktopModeFlags; import android.window.PictureInPictureSurfaceTransaction; +import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.graphics.ColorUtils; +import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.jank.Cuj; import com.android.launcher3.AbstractFloatingView; @@ -137,7 +146,6 @@ import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; import com.android.launcher3.Insettable; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -153,6 +161,7 @@ import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statehandlers.DepthController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StatefulContainer; @@ -173,8 +182,11 @@ import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.TranslateEdgeEffect; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.ViewPool; +import com.android.launcher3.util.coroutines.DispatcherProvider; +import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener; import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.GestureState; +import com.android.quickstep.HighResLoadingState; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.RecentsAnimationTargets; @@ -187,24 +199,31 @@ import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SplitSelectionListener; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskOverlayFactory; -import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.TaskViewUtils; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.ViewUtils; +import com.android.quickstep.fallback.window.RecentsWindowFlags; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.recents.data.TasksRepository; +import com.android.quickstep.recents.data.RecentTasksRepository; +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository; +import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl; +import com.android.quickstep.recents.data.RecentsRotationStateRepository; +import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl; +import com.android.quickstep.recents.di.RecentsDependencies; import com.android.quickstep.recents.viewmodel.RecentsViewData; -import com.android.quickstep.util.ActiveGestureErrorDetector; -import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.recents.viewmodel.RecentsViewModel; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsAtomicAnimationFactory; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SingleTask; import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; +import com.android.quickstep.util.SplitTask; import com.android.quickstep.util.SurfaceTransaction; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskGridNavHelper; @@ -212,47 +231,55 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.VibrationConstants; -import com.android.quickstep.views.TaskView.TaskContainer; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.common.pip.IPipAnimationListener; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.GroupedTaskInfo; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import kotlin.Unit; +import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function0; + +import kotlinx.coroutines.CoroutineScope; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; - /** * A list of recent tasks. + * * @param : the container that should host recents view - * @param : the type of base state that will be used + * @param : the type of base state that will be used */ - -public abstract class RecentsView, STATE_TYPE extends BaseState> extends PagedView implements Insettable, - TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, - TaskVisualsChangeListener { + HighResLoadingState.HighResLoadingStateChangedCallback, + TaskVisualsChangeListener, DesktopVisibilityListener { - private static final String TAG = "RecentsView"; + protected static final String TAG = "RecentsView"; private static final boolean DEBUG = false; - public static final FloatProperty CONTENT_ALPHA = - new FloatProperty("contentAlpha") { + public static final FloatProperty> CONTENT_ALPHA = + new FloatProperty<>("contentAlpha") { @Override public void setValue(RecentsView view, float v) { view.setContentAlpha(v); @@ -264,8 +291,8 @@ public abstract class RecentsView FULLSCREEN_PROGRESS = - new FloatProperty("fullscreenProgress") { + public static final FloatProperty> FULLSCREEN_PROGRESS = + new FloatProperty<>("fullscreenProgress") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setFullscreenProgress(v); @@ -277,8 +304,8 @@ public abstract class RecentsView TASK_MODALNESS = - new FloatProperty("taskModalness") { + public static final FloatProperty> TASK_MODALNESS = + new FloatProperty<>("taskModalness") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskModalness(v); @@ -290,8 +317,8 @@ public abstract class RecentsView ADJACENT_PAGE_HORIZONTAL_OFFSET = - new FloatProperty("adjacentPageHorizontalOffset") { + public static final FloatProperty> ADJACENT_PAGE_HORIZONTAL_OFFSET = + new FloatProperty<>("adjacentPageHorizontalOffset") { @Override public void setValue(RecentsView recentsView, float v) { if (recentsView.mAdjacentPageHorizontalOffset != v) { @@ -306,8 +333,22 @@ public abstract class RecentsView> RUNNING_TASK_ATTACH_ALPHA = + new FloatProperty<>("runningTaskAttachAlpha") { + @Override + public void setValue(RecentsView recentsView, float v) { + recentsView.mRunningTaskAttachAlpha = v; + recentsView.applyAttachAlpha(); + } + + @Override + public Float get(RecentsView recentsView) { + return recentsView.mRunningTaskAttachAlpha; + } + }; + public static final int SCROLL_VIBRATION_PRIMITIVE = - Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1; + VibrationEffect.Composition.PRIMITIVE_LOW_TICK; public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f; public static final VibrationEffect SCROLL_VIBRATION_FALLBACK = VibrationConstants.EFFECT_TEXTURE_TICK; @@ -316,10 +357,9 @@ public abstract class RecentsView COLOR_TINT = - new FloatProperty("colorTint") { + private static final FloatProperty> COLOR_TINT = + new FloatProperty<>("colorTint") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setColorTint(v); @@ -337,8 +377,8 @@ public abstract class RecentsView TASK_SECONDARY_TRANSLATION = - new FloatProperty("taskSecondaryTranslation") { + public static final FloatProperty> TASK_SECONDARY_TRANSLATION = + new FloatProperty<>("taskSecondaryTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsResistanceTranslation(v); @@ -356,8 +396,8 @@ public abstract class RecentsView TASK_PRIMARY_SPLIT_TRANSLATION = - new FloatProperty("taskPrimarySplitTranslation") { + public static final FloatProperty> TASK_PRIMARY_SPLIT_TRANSLATION = + new FloatProperty<>("taskPrimarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsPrimarySplitTranslation(v); @@ -369,8 +409,8 @@ public abstract class RecentsView TASK_SECONDARY_SPLIT_TRANSLATION = - new FloatProperty("taskSecondarySplitTranslation") { + public static final FloatProperty> TASK_SECONDARY_SPLIT_TRANSLATION = + new FloatProperty<>("taskSecondarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsSecondarySplitTranslation(v); @@ -383,15 +423,12 @@ public abstract class RecentsView RECENTS_SCALE_PROPERTY = - new FloatProperty("recentsScale") { + public static final FloatProperty> RECENTS_SCALE_PROPERTY = + new FloatProperty<>("recentsScale") { @Override public void setValue(RecentsView view, float scale) { view.setScaleX(scale); view.setScaleY(scale); - if (enableRefactorTaskThumbnail()) { - view.mRecentsViewData.getScale().setValue(scale); - } view.mLastComputedTaskStartPushOutDistance = null; view.mLastComputedTaskEndPushOutDistance = null; view.runActionOnRemoteHandles(new Consumer() { @@ -416,8 +453,8 @@ public abstract class RecentsView RECENTS_GRID_PROGRESS = - new FloatProperty("recentsGrid") { + public static final FloatProperty> RECENTS_GRID_PROGRESS = + new FloatProperty<>("recentsGrid") { @Override public void setValue(RecentsView view, float gridProgress) { view.setGridProgress(gridProgress); @@ -429,12 +466,27 @@ public abstract class RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS = + new FloatProperty<>("desktopCarouselDetachProgress") { + @Override + public void setValue(RecentsView view, float offset) { + view.mDesktopCarouselDetachProgress = offset; + view.applyAttachAlpha(); + view.updatePageOffsets(); + } + + @Override + public Float get(RecentsView view) { + return view.mDesktopCarouselDetachProgress; + } + }; + /** * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and * being in any other state has a value of 0. */ - public static final FloatProperty TASK_THUMBNAIL_SPLASH_ALPHA = - new FloatProperty("taskThumbnailSplashAlpha") { + public static final FloatProperty> TASK_THUMBNAIL_SPLASH_ALPHA = + new FloatProperty<>("taskThumbnailSplashAlpha") { @Override public void setValue(RecentsView view, float taskThumbnailSplashAlpha) { view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha); @@ -462,11 +514,8 @@ public abstract class RecentsView mSizeStrategy; + protected final BaseContainerInterface mSizeStrategy; @Nullable protected RecentsAnimationController mRecentsAnimationController; @Nullable @@ -482,11 +531,9 @@ public abstract class RecentsView mGroupedTaskViewPool; private final ViewPool mDesktopTaskViewPool; - private final TaskOverlayFactory mTaskOverlayFactory; + protected final TaskOverlayFactory mTaskOverlayFactory; protected boolean mDisallowScrollToClearAll; + // True if it is not allowed to scroll to [AddDesktopButton]. + protected boolean mDisallowScrollToAddDesk; private boolean mOverlayEnabled; protected boolean mFreezeViewVisibility; private boolean mOverviewGridEnabled; @@ -543,21 +598,22 @@ public abstract class RecentsView mTaskViewsDismissPrimaryTranslations = + new HashMap(); + /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ @@ -603,25 +662,28 @@ public abstract class RecentsView( () -> PackageManagerWrapper.getInstance() .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null, MAIN_EXECUTOR, apkRemoved -> { if (apkRemoved) { - dismissTask(taskId); + dismissTask(taskId, /*animate=*/true, /*removeTask=*/false); } else { mModel.isTaskRemoved(taskKey.id, taskRemoved -> { if (taskRemoved) { - dismissTask(taskId); + dismissTask(taskId, /*animate=*/true, /*removeTask=*/false); } }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter())); } @@ -646,12 +708,11 @@ public abstract class RecentsView new RecentsRotationStateRepositoryImpl(mOrientationState)); + + recentsDependencies.provide(RecentsDeviceProfileRepository.class, scopeId, + () -> new RecentsDeviceProfileRepositoryImpl(mContainer)); + } else { + mRecentsViewModel = null; + mHelper = null; + } + mScrollHapticMinGapMillis = getResources() .getInteger(R.integer.recentsScrollHapticMinGapMillis); mFastFlingVelocity = getResources() .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); mModel = RecentsModel.INSTANCE.get(context); - mIdp = InvariantDeviceProfile.INSTANCE.get(context); - if (enableRefactorTaskThumbnail()) { - mTasksRepository = new TasksRepository( - mModel, mModel.getThumbnailCache(), mModel.getIconCache()); - } else { - mTasksRepository = null; - } mClearAllButton = (ClearAllButton) LayoutInflater.from(context) .inflate(R.layout.overview_clear_all_button, this, false); mClearAllButton.setOnClickListener(this::dismissAllTasks); + + if (DesktopModeStatus.enableMultipleDesktops(mContext)) { + mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate( + R.layout.overview_add_desktop_button, this, false); + mAddDesktopButton.setOnClickListener(this::createDesk); + + mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(mContext); + } + mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 10 /* initial size */); + int groupedViewPoolInitialSize = enableRefactorTaskThumbnail() ? 2 : 10; mGroupedTaskViewPool = new ViewPool<>(context, this, - R.layout.task_grouped, 20 /* max size */, 10 /* initial size */); + R.layout.task_grouped, 20 /* max size */, groupedViewPoolInitialSize); + int desktopViewPoolInitialSize = DesktopModeStatus.canEnterDesktopMode(mContext) ? 1 : 0; mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop, - 5 /* max size */, 1 /* initial size */); + 5 /* max size */, desktopViewPoolInitialSize); setOrientationHandler(mOrientationState.getOrientationHandler()); mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources()); @@ -847,8 +957,11 @@ public abstract class RecentsView container.getIconView().getDrawable() != null)) { - tv.onTaskListVisibilityChanged(true /* visible */); + taskView.onTaskListVisibilityChanged(true /* visible */); } } } @@ -1049,42 +1167,36 @@ public abstract class RecentsView thumbnailData, boolean refreshNow) { - TaskView updatedTaskView = null; - for (Map.Entry entry : thumbnailData.entrySet()) { - Integer id = entry.getKey(); - ThumbnailData thumbnail = entry.getValue(); - TaskView taskView = getTaskViewByTaskId(id); - if (taskView == null) { - continue; + /** Updates the thumbnail(s) of the relevant TaskView. */ + public void updateThumbnail(Map thumbnailData) { + if (!enableRefactorTaskThumbnail()) { + for (Map.Entry entry : thumbnailData.entrySet()) { + Integer id = entry.getKey(); + ThumbnailData thumbnail = entry.getValue(); + TaskView taskView = getTaskViewByTaskId(id); + if (taskView == null) { + continue; + } + // taskView could be a GroupedTaskView, so select the relevant task by ID + TaskContainer taskContainer = taskView.getTaskContainerById(id); + if (taskContainer == null) { + continue; + } + Task task = taskContainer.getTask(); + TaskThumbnailViewDeprecated taskThumbnailViewDeprecated = + taskContainer.getThumbnailViewDeprecated(); + taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, /*refreshNow=*/false); } - // taskView could be a GroupedTaskView, so select the relevant task by ID - TaskContainer taskAttributes = taskView.getTaskContainerById(id); - if (taskAttributes == null) { - continue; - } - Task task = taskAttributes.getTask(); - TaskThumbnailViewDeprecated taskThumbnailViewDeprecated = - taskAttributes.getThumbnailViewDeprecated(); - taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow); - // thumbnailData can contain 1-2 ids, but they should correspond to the same - // TaskView, so overwriting is ok - updatedTaskView = taskView; } - - return updatedTaskView; } @Override @@ -1096,7 +1208,7 @@ public abstract class RecentsView remoteTargetHandle.getTransformParams() .setSyncTransactionApplier(mSyncTransactionApplier)); - RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); + RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this); mIPipAnimationListener.setActivityAndRecentsView(mContainer, this); - SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener( + SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener( mIPipAnimationListener); mOrientationState.initListeners(); mTaskOverlayFactory.initListeners(); - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.registerSplitListener(mSplitSelectionListener); + mSplitSelectStateController.registerSplitListener(mSplitSelectionListener); + if (mDesktopVisibilityController != null) { + mDesktopVisibilityController.registerDesktopVisibilityListener(this); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); @@ -1151,51 +1266,81 @@ public abstract class RecentsView remoteTargetHandle.getTransformParams() .setSyncTransactionApplier(null)); executeSideTaskLaunchCallback(); - RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); - SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null); + RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this); + SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null); mIPipAnimationListener.setActivityAndRecentsView(null, null); mOrientationState.destroyListeners(); mTaskOverlayFactory.removeListeners(); - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener); + mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener); + if (mDesktopVisibilityController != null) { + mDesktopVisibilityController.unregisterDesktopVisibilityListener(this); } reset(); } - @Override - public void onViewRemoved(View child) { - super.onViewRemoved(child); - - // Clear the task data for the removed child if it was visible unless: - // - It's the initial taskview for entering split screen, we only pretend to dismiss the - // task - // - It's the focused task to be moved to the front, we immediately re-add the task - if (child instanceof TaskView && child != mSplitHiddenTaskView - && child != mMovingTaskView) { - TaskView taskView = (TaskView) child; - for (int i : taskView.getTaskIds()) { - mHasVisibleTaskData.delete(i); + /** + * Execute clean-up logic needed when the view is destroyed. + */ + public void destroy() { + Log.d(TAG, "destroy"); + if (enableRefactorTaskThumbnail()) { + try { + mTaskViewPool.killOngoingInitializations(); + mGroupedTaskViewPool.killOngoingInitializations(); + mDesktopTaskViewPool.killOngoingInitializations(); + } catch (InterruptedException e) { + Log.e(TAG, "Ongoing initializations could not be killed", e); } - if (child instanceof GroupedTaskView) { - mGroupedTaskViewPool.recycle((GroupedTaskView) taskView); - } else if (child instanceof DesktopTaskView) { - mDesktopTaskViewPool.recycle((DesktopTaskView) taskView); - } else { - mTaskViewPool.recycle(taskView); - } - mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); + mHelper.onDestroy(); + RecentsDependencies.destroy(getContext()); } } + @Override + public void onViewRemoved(View child) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewRemoved"); + super.onViewRemoved(child); + // Clear the task data for the removed child if it was visible unless: + // - It's the initial taskview for entering split screen, we only pretend to dismiss the + // task + // - It's the focused task to be moved to the front, we immediately re-add the task + if (child instanceof TaskView) { + mTaskViewCount = Math.max(0, --mTaskViewCount); + if (child != mSplitHiddenTaskView && child != mMovingTaskView) { + clearAndRecycleTaskView((TaskView) child); + } + } + traceEnd(Trace.TRACE_TAG_APP); + } + + private void clearAndRecycleTaskView(TaskView taskView) { + for (int i : taskView.getTaskIds()) { + mHasVisibleTaskData.delete(i); + } + if (taskView instanceof GroupedTaskView) { + mGroupedTaskViewPool.recycle((GroupedTaskView) taskView); + } else if (taskView instanceof DesktopTaskView) { + mDesktopTaskViewPool.recycle((DesktopTaskView) taskView); + } else { + mTaskViewPool.recycle(taskView); + } + mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews()); + } + @Override public void onViewAdded(View child) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewAdded"); super.onViewAdded(child); + if (child instanceof TaskView) { + mTaskViewCount++; + } child.setAlpha(mContentAlpha); // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the // child direction back to match system settings. child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL); mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false); updateEmptyMessage(); + traceEnd(Trace.TRACE_TAG_APP); } @Override @@ -1274,12 +1419,13 @@ public abstract class RecentsView= 0; --i) { + if (!apps[i].isTranslucent) { + return false; + } + } + return true; + } + public boolean isTaskViewVisible(TaskView tv) { if (showAsGrid()) { int screenStart = getPagedOrientationHandler().getPrimaryScroll(this); int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this); - return isTaskViewWithinBounds(tv, screenStart, screenEnd); + return isTaskViewWithinBounds(tv, screenStart, screenEnd, /*taskViewTranslation=*/ 0); } else { // For now, just check if it's the active task or an adjacent task return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; @@ -1360,7 +1519,7 @@ public abstract class RecentsView= start && taskStart <= end) || (taskEnd >= start - && taskEnd <= end); + + int translatedTaskStart = taskStart + taskViewTranslation; + int translatedTaskEnd = taskEnd + taskViewTranslation; + + taskStart = Math.min(taskStart, translatedTaskStart); + taskEnd = Math.max(taskEnd, translatedTaskEnd); + + return (taskStart >= screenStart && taskStart <= screenEnd) || (taskEnd >= screenStart + && taskEnd <= screenEnd); } private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) { @@ -1407,17 +1593,19 @@ public abstract class RecentsView taskGroups) { if (mPendingAnimation != null) { - mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); + final List finalTaskGroups = taskGroups; + mPendingAnimation.addEndListener(success -> applyLoadPlan(finalTaskGroups)); return; } @@ -1747,7 +1926,7 @@ public abstract class RecentsView= 0; i--) { GroupTask groupTask = taskGroups.get(i); - boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID + boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID && groupTask.containsTask(stagedTaskIdToBeRemoved); + boolean shouldSkipGroupTask = containsStagedTask && groupTask instanceof SingleTask; - if (isRemovalNeeded && !groupTask.hasMultipleTasks()) { - // If the task we need to remove is not part of a pair, avoiding creating the - // TaskView. + if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP) + || shouldSkipGroupTask) { + // To avoid these tasks from being chosen as the app pair, the creation of a + // TaskView is bypassed. The staged task is already selected for the app pair, + // and the Desktop task should be hidden when selecting a pair. continue; } // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE // to be a temporary container for the remaining task. + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.createTaskView"); TaskView taskView = getTaskViewFromPool( - isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType); - if (taskView instanceof GroupedTaskView) { - boolean firstTaskIsLeftTopTask = - groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; - Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; - Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; - ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState, - mTaskOverlayFactory, groupTask.mSplitBounds); - } else if (taskView instanceof DesktopTaskView) { - ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks, - mOrientationState, mTaskOverlayFactory); - mDesktopTaskView = (DesktopTaskView) taskView; - } else { - Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2 - : groupTask.task1; + containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType); + traceEnd(Trace.TRACE_TAG_APP); + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.bind"); + if (taskView instanceof GroupedTaskView groupedTaskView) { + var splitTask = (SplitTask) groupTask; + groupedTaskView.bind(splitTask.getTopLeftTask(), + splitTask.getBottomRightTask(), mOrientationState, + mTaskOverlayFactory, splitTask.getSplitBounds()); + } else if (taskView instanceof DesktopTaskView desktopTaskView) { + desktopTaskView.bind((DesktopTask) groupTask, mOrientationState, + mTaskOverlayFactory); + } else if (groupTask instanceof SplitTask splitTask) { + Task task = splitTask.getTopLeftTask().key.id == stagedTaskIdToBeRemoved + ? splitTask.getBottomRightTask() + : splitTask.getTopLeftTask(); taskView.bind(task, mOrientationState, mTaskOverlayFactory); + } else { + taskView.bind(((SingleTask) groupTask).getTask(), mOrientationState, + mTaskOverlayFactory); } + traceEnd(Trace.TRACE_TAG_APP); + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.addTaskView"); addView(taskView); + traceEnd(Trace.TRACE_TAG_APP); // enables instance filtering if the feature flag for it is on if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) { taskView.setUpShowAllInstancesListener(); } } + // For loop end trace + traceEnd(Trace.TRACE_TAG_APP); - if (!taskGroups.isEmpty()) { - addView(mClearAllButton); - } + addView(mClearAllButton); // Keep same previous focused task - TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds); - // If the list changed, maybe the focused task doesn't exist anymore - if (newFocusedTaskView == null && getTaskViewCount() > 0) { - newFocusedTaskView = getTaskViewAt(0); + TaskView newFocusedTaskView = null; + if (!enableGridOnlyOverview()) { + newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds); + if (enableLargeDesktopWindowingTile() + && newFocusedTaskView instanceof DesktopTaskView) { + newFocusedTaskView = null; + } + // If the list changed, maybe the focused task doesn't exist anymore. + if (newFocusedTaskView == null) { + newFocusedTaskView = mUtils.getFirstNonDesktopTaskView(); + } } - mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview() - ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID; - updateTaskSize(); - updateChildTaskOrientations(); + setFocusedTaskViewId( + newFocusedTaskView != null ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID); - TaskView newRunningTaskView = null; - if (hasAllValidTaskIds(runningTaskIds)) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.layouts"); + updateTaskSize(); + mUtils.updateChildTaskOrientations(); + traceEnd(Trace.TRACE_TAG_APP); + + TaskView newRunningTaskView = mUtils.getDesktopTaskViewForDeskId(runningTaskViewDeskId); + if (newRunningTaskView == null) { // Update mRunningTaskViewId to be the new TaskView that was assigned by binding // the full list of tasks to taskViews newRunningTaskView = getTaskViewByTaskIds(runningTaskIds); - if (newRunningTaskView != null) { - setRunningTaskViewId(newRunningTaskView.getTaskViewId()); + } + if (newRunningTaskView != null) { + setRunningTaskViewId(newRunningTaskView.getTaskViewId()); + } else { + if (mActiveGestureGroupedTaskInfo != null) { + // This will update mRunningTaskViewId and create a stub view if necessary. + // We try to avoid this because it can cause a scroll jump, but it is needed + // for cases where the running task isn't included in this load plan (e.g. if + // the current running task is excludedFromRecents.) + showCurrentTask(mActiveGestureGroupedTaskInfo, "applyLoadPlan"); + newRunningTaskView = getRunningTaskView(); } else { - if (mActiveGestureRunningTasks != null) { - // This will update mRunningTaskViewId and create a stub view if necessary. - // We try to avoid this because it can cause a scroll jump, but it is needed - // for cases where the running task isn't included in this load plan (e.g. if - // the current running task is excludedFromRecents.) - showCurrentTask(mActiveGestureRunningTasks); - } else { - setRunningTaskViewId(INVALID_TASK_ID); - } + setRunningTaskViewId(INVALID_TASK_ID); } } @@ -1883,21 +2117,18 @@ public abstract class RecentsView 0) { - targetPage = indexOfChild(requireTaskViewAt(0)); - } + targetPage = indexOfChild( + mUtils.getExpectedCurrentTask(newRunningTaskView, newFocusedTaskView)); } if (targetPage != -1 && mCurrentPage != targetPage) { int finalTargetPage = targetPage; @@ -1914,6 +2145,7 @@ public abstract class RecentsView= 0; i--) { - if (i == stubRunningTaskIndex) { - continue; - } - removeView(requireTaskViewAt(i)); - } - if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) { + private void removeAllTaskViews() { + // This handles an edge case where applyLoadPlan happens during a gesture when the only + // Task is one with excludeFromRecents, in which case we should not remove it. + CollectionsKt + .filter(getTaskViews(), taskView -> !isGestureActive() || !taskView.isRunningTask()) + .forEach(this::removeView); + if (!hasTaskViews()) { + removeView(mAddDesktopButton); removeView(mClearAllButton); } } - public int getTaskViewCount() { - int taskViewCount = getChildCount(); - if (indexOfChild(mClearAllButton) != -1) { - taskViewCount--; - } - return taskViewCount; + /** Returns true if there are at least one TaskView has been added to the RecentsView. */ + public boolean hasTaskViews() { + return mUtils.hasTaskViews(); } - public int getGroupedTaskViewCount() { - int groupViewCount = 0; - for (int i = 0; i < getChildCount(); i++) { - if (getChildAt(i) instanceof GroupedTaskView) { - groupViewCount++; - } - } - return groupViewCount; + public int getTaskViewCount() { + return mTaskViewCount; + } + + /** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */ + public int getNonDesktopTaskViewCount() { + return mUtils.getNonDesktopTaskViewCount(); } /** @@ -1991,16 +2220,16 @@ public abstract class RecentsView= 0; i--) { - TaskView taskView = requireTaskViewAt(i); + for (TaskView taskView : getTaskViews()) { if (Arrays.stream(taskView.getTaskIds()).noneMatch( taskId -> taskId == mIgnoreResetTaskId)) { taskView.resetViewTransforms(); - taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); + taskView.setIconVisibleForGesture(mTaskIconVisible); taskView.setStableAlpha(mContentAlpha); taskView.setFullscreenProgress(mFullscreenProgress); taskView.setModalness(mTaskModalness); taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha); + taskView.setBorderEnabled(mBorderEnabled); } } // resetTaskVisuals is called at the end of dismiss animation which could update @@ -2013,14 +2242,10 @@ public abstract class RecentsView visibleTaskIds = new ArrayList<>(); - // Update the task data for the in/visible children - for (int i = 0; i < getTaskViewCount(); i++) { - TaskView taskView = requireTaskViewAt(i); + getTaskViews().forEachWithIndexInParent((index, taskView) -> { List containers = taskView.getTaskContainers(); if (containers.isEmpty()) { - continue; + return; } - int index = indexOfChild(taskView); boolean visible; if (showAsGrid()) { - visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd); + visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd, + mTaskViewsDismissPrimaryTranslations.getOrDefault(taskView, 0)); } else { - visible = lower <= index && index <= upper; + visible = index >= lowerIndex && index <= upperIndex; } if (visible) { // Default update all non-null tasks, then remove running ones @@ -2404,16 +2629,10 @@ public abstract class RecentsView task.key.id).toList()); } - if (mTmpRunningTasks != null) { - for (Task t : mTmpRunningTasks) { - // Skip loading if this is the task that we are animating into - // TODO(b/280812109) change this equality check to use A.equals(B) - tasksToUpdate.removeIf(task -> task == t); - } - } if (tasksToUpdate.isEmpty()) { - continue; + return; } + int visibilityChanges = 0; for (Task task : tasksToUpdate) { if (!mHasVisibleTaskData.get(task.key.id)) { // Ignore thumbnail update if it's current running task during the gesture @@ -2422,25 +2641,32 @@ public abstract class RecentsView - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false)); - if (!FeatureFlags.enableSplitContextually()) { - resetFromSplitSelectionState(); - } - + mBlurUtils.setDrawLiveTileBelowRecents(false); // These are relatively expensive and don't need to be done this frame (RecentsView isn't // visible anyway), so defer by a frame to get off the critical path, e.g. app to home. - post(() -> { - unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); - setCurrentPage(0); - LayoutUtils.setViewEnabled(mActionsView, true); - if (mOrientationState.setGestureActive(false)) { - updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); - } - }); + post(this::onReset); + } + + private void onReset() { + unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); + setCurrentPage(0); + LayoutUtils.setViewEnabled(mActionsView, true); + if (mOrientationState.setGestureActive(false)) { + updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); + } + if (enableRefactorTaskThumbnail()) { + mRecentsViewModel.onReset(); + } } public int getRunningTaskViewId() { @@ -2564,13 +2801,12 @@ public abstract class RecentsView endState = mSizeStrategy.stateFromGestureEndTarget(endTarget); + // Starting the desk exploded animation when the gesture from an app is released. + if (enableDesktopExplodedView()) { + if (animatorSet == null) { + mUtils.setDeskExplodeProgress(endState.showExplodedDesktopView() ? 1f : 0f); + } else { + animatorSet.play( + ObjectAnimator.ofFloat(this, DESK_EXPLODE_PROGRESS, + endState.showExplodedDesktopView() ? 1f : 0f)); + } + + for (TaskView taskView : getTaskViews()) { + if (taskView instanceof DesktopTaskView desktopTaskView) { + desktopTaskView.setRemoteTargetHandles(remoteTargetHandles); + } + } + } + if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) { TaskView runningTaskView = getRunningTaskView(); - float runningTaskPrimaryGridTranslation = 0; - float runningTaskSecondaryGridTranslation = 0; + float runningTaskGridTranslationX = 0; + float runningTaskGridTranslationY = 0; if (runningTaskView != null) { // Apply the grid translation to running task unless it's being snapped to // and removes the current translation applied to the running task. - runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX() + runningTaskGridTranslationX = runningTaskView.getGridTranslationX() - runningTaskView.getNonGridTranslationX(); - runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY(); + runningTaskGridTranslationY = runningTaskView.getGridTranslationY(); } - for (TaskViewSimulator tvs : taskViewSimulators) { + for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) { + TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator(); if (animatorSet == null) { setGridProgress(1); - tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation; - tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation; + if (enableGridOnlyOverview()) { + tvs.taskGridTranslationX.value = runningTaskGridTranslationX; + tvs.taskGridTranslationY.value = runningTaskGridTranslationY; + } else { + tvs.taskPrimaryTranslation.value = runningTaskGridTranslationX; + tvs.taskSecondaryTranslation.value = runningTaskGridTranslationY; + } } else { animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1)); - animatorSet.play(tvs.carouselScale.animateToValue(1)); - animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0)); - animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0)); - animatorSet.play(tvs.taskPrimaryTranslation.animateToValue( - runningTaskPrimaryGridTranslation)); - animatorSet.play(tvs.taskSecondaryTranslation.animateToValue( - runningTaskSecondaryGridTranslation)); + if (enableGridOnlyOverview()) { + animatorSet.play(tvs.carouselScale.animateToValue(1)); + animatorSet.play(tvs.taskGridTranslationX.animateToValue( + runningTaskGridTranslationX)); + animatorSet.play(tvs.taskGridTranslationY.animateToValue( + runningTaskGridTranslationY)); + } else { + animatorSet.play(tvs.taskPrimaryTranslation.animateToValue( + runningTaskGridTranslationX)); + animatorSet.play(tvs.taskSecondaryTranslation.animateToValue( + runningTaskGridTranslationY)); + } } } } @@ -2772,13 +3032,21 @@ public abstract class RecentsView { + TaskViewSimulator taskViewSimulator = remoteTargetHandle.getTaskViewSimulator(); + // After settling in Overview, recentsScroll will be used to adjust horizontally + // location and taskGridTranslationX doesn't needs to be applied. + taskViewSimulator.taskGridTranslationX.value = 0; + taskViewSimulator.taskGridTranslationY.value = + runningTaskView.getGridTranslationY(); + }); + } + } + mCurrentGestureEndTarget = null; } /** * Returns true if we should add a stub taskView for the running task id */ - protected boolean shouldAddStubTaskView(Task[] runningTasks) { - int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray(); + protected boolean shouldAddStubTaskView(GroupedTaskInfo groupedTaskInfo) { + int[] runningTaskIds; + if (groupedTaskInfo != null) { + runningTaskIds = groupedTaskInfo.getTaskInfoList().stream().mapToInt( + taskInfo -> taskInfo.taskId).toArray(); + } else { + runningTaskIds = new int[0]; + } TaskView matchingTaskView = null; - if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) { - // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single + if (groupedTaskInfo != null && groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK) + && runningTaskIds.length == 1) { + // TODO(b/342635213): Unsure if it's expected, desktop runningTasks only have a single // taskId, therefore we match any DesktopTaskView that contains the runningTaskId. TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]); if (taskview instanceof DesktopTaskView) { @@ -2813,44 +3109,41 @@ public abstract class RecentsView 1; - boolean needDesktopTask = hasDesktopTask(runningTasks); - if (shouldAddStubTaskView(runningTasks)) { + if (shouldAddStubTaskView(groupedTaskInfo)) { boolean wasEmpty = getChildCount() == 0; // Add an empty view for now until the task plan is loaded and applied final TaskView taskView; - if (needDesktopTask) { - taskView = getTaskViewFromPool(TaskView.Type.DESKTOP); - mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length); - ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks), - mOrientationState, mTaskOverlayFactory); - } else if (needGroupTaskView) { - taskView = getTaskViewFromPool(TaskView.Type.GROUPED); - mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]}; + if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)) { + taskView = mUtils.createDesktopTaskViewForActiveDesk(groupedTaskInfo); + } else if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_SPLIT)) { + taskView = getTaskViewFromPool(TaskViewType.GROUPED); // When we create a placeholder task view mSplitBoundsConfig will be null, but with // the actual app running we won't need to show the thumbnail until all the tasks // load later anyways - ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1], - mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig); + ((GroupedTaskView) taskView).bind(Task.from(groupedTaskInfo.getTaskInfo1()), + Task.from(groupedTaskInfo.getTaskInfo2()), mOrientationState, + mTaskOverlayFactory, mSplitBoundsConfig); } else { - taskView = getTaskViewFromPool(TaskView.Type.SINGLE); - // The temporary running task is only used for the duration between the start of the - // gesture and the task list is loaded and applied - mTmpRunningTasks = new Task[]{runningTasks[0]}; - taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory); + taskView = getTaskViewFromPool(TaskViewType.SINGLE); + taskView.bind(Task.from(groupedTaskInfo.getTaskInfo1()), mOrientationState, + mTaskOverlayFactory); } - addView(taskView, 0); + if (mAddDesktopButton != null && wasEmpty) { + addView(mAddDesktopButton); + } + addView(taskView, mUtils.getRunningTaskExpectedIndex(taskView)); runningTaskViewId = taskView.getTaskViewId(); if (wasEmpty) { addView(mClearAllButton); @@ -2861,36 +3154,40 @@ public abstract class RecentsView setCurrentPage(getRunningTaskIndex())); setRunningTaskViewShowScreenshot(false); setRunningTaskHidden(runningTaskTileHidden); // Update task size after setting current task. updateTaskSize(); - updateChildTaskOrientations(); + mUtils.updateChildTaskOrientations(); // Reload the task list reloadIfNeeded(); } - private boolean hasDesktopTask(Task[] runningTasks) { - if (!DesktopModeStatus.canEnterDesktopMode(mContext)) { - return false; - } - for (Task task : runningTasks) { - if (task.key.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { - return true; - } - } - return false; - } - /** * Sets the running task id, cleaning up the old running task if necessary. */ @@ -2901,7 +3198,7 @@ public abstract class RecentsView updatedThumbnails) { mRunningTaskShowScreenshot = showScreenshot; TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { - runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot); + runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot, updatedThumbnails); + } + if (enableRefactorTaskThumbnail()) { + mRecentsViewModel.setRunningTaskShowScreenshot(showScreenshot); } } - public void setTaskIconScaledDown(boolean isScaledDown) { - if (mTaskIconScaledDown != isScaledDown) { - mTaskIconScaledDown = isScaledDown; - int taskCount = getTaskViewCount(); - for (int i = 0; i < taskCount; i++) { - requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); + /** + * Updates icon visibility when going in or out of overview. + */ + public void setTaskIconVisible(boolean isVisible) { + if (mTaskIconVisible != isVisible) { + mTaskIconVisible = isVisible; + for (TaskView taskView : getTaskViews()) { + taskView.setIconVisibleForGesture(mTaskIconVisible); } } } private void animateActionsViewIn() { if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) { - animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION); + animateActionsViewAlpha(1, TaskView.FADE_IN_ICON_DURATION); } } - public void animateUpTaskIconScale() { - mTaskIconScaledDown = false; - int taskCount = getTaskViewCount(); - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); - taskView.animateIconScaleAndDimIntoView(); + /** + * Updates icon visibility when gesture is settled. + */ + public void startIconFadeInOnGestureComplete() { + mTaskIconVisible = true; + for (TaskView taskView : getTaskViews()) { + taskView.startIconFadeInOnGestureComplete(); } } /** * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid * layout. - * This method is used when no task dismissal has occurred. + * + * Skips rebalance. */ private void updateGridProperties() { - updateGridProperties(false, Integer.MAX_VALUE); - } - - /** - * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid - * layout. - * - * This method is used when task dismissal has occurred, but rebalance is not needed. - * - * @param isTaskDismissal indicates if update was called due to task dismissal - */ - private void updateGridProperties(boolean isTaskDismissal) { - updateGridProperties(isTaskDismissal, Integer.MAX_VALUE); + updateGridProperties(null); } /** @@ -3005,13 +3312,11 @@ public abstract class RecentsView gridTranslations = new HashMap<>(); - // Horizontal grid translation for each task - float[] gridTranslations = new float[taskCount]; - - int focusedTaskIndex = Integer.MAX_VALUE; - int focusedTaskShift = 0; - int focusedTaskWidthAndSpacing = 0; + TaskView lastLargeTaskView = mUtils.getLastLargeTaskView(); + int focusedTaskViewShift = 0; + int largeTaskWidthAndSpacing = 0; int snappedTaskRowWidth = 0; + int expectedCurrentTaskRowWidth = 0; int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage(); TaskView snappedTaskView = getTaskViewAt(snappedPage); TaskView homeTaskView = getHomeTaskView(); + TaskView expectedCurrentTaskView = mUtils.getExpectedCurrentTask(getRunningTaskView(), + getFocusedTaskView()); TaskView nextFocusedTaskView = null; - if (!isTaskDismissal) { + // Don't clear the top row, if the user has dismissed a task, to maintain the task order. + if (!mAnyTaskHasBeenDismissed) { mTopRowIdSet.clear(); } - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); + + // Consecutive task views in the top row or bottom row, which means another one set will + // be cleared up while starting to add TaskViews to one of them. Also means only one of + // them can be non-empty at most. + Set lastTopTaskViews = new HashSet<>(); + Set lastBottomTaskViews = new HashSet<>(); + + int largeTasksCount = 0; + // True if the last large TaskView has been visited during the TaskViews iteration. + boolean encounteredLastLargeTaskView = false; + // True if the highest index visible TaskView has been visited during the TaskViews + // iteration. + boolean encounteredLastVisibleTaskView = false; + for (TaskView taskView : getTaskViews()) { + if (taskView == lastLargeTaskView) { + encounteredLastLargeTaskView = true; + } + if (taskView == lastVisibleTaskViewDuringDismiss) { + encounteredLastVisibleTaskView = true; + } + float gridTranslation = 0f; int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing; // Evenly distribute tasks between rows unless rearranging due to task dismissal, in // which case keep tasks in their respective rows. For the running task, don't join // the grid. - if (taskView.isFocusedTask()) { - topRowWidth += taskWidthAndSpacing; - bottomRowWidth += taskWidthAndSpacing; - - focusedTaskIndex = i; - focusedTaskWidthAndSpacing = taskWidthAndSpacing; - gridTranslations[i] += focusedTaskShift; - gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; + if (taskView.isLargeTile()) { + largeTasksCount++; + // DesktopTaskView`s are hidden during split select state, so we shouldn't count + // them when calculating row width. + if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) { + topRowWidth += taskWidthAndSpacing; + bottomRowWidth += taskWidthAndSpacing; + largeTileRowWidth += taskWidthAndSpacing; + } + gridTranslation += focusedTaskViewShift; + gridTranslation += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; // Center view vertically in case it's from different orientation. taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin - taskView.getLayoutParams().height) / 2f); + largeTaskWidthAndSpacing = taskWidthAndSpacing; + if (taskView == snappedTaskView) { - // If focused task is snapped, the row width is just task width and spacing. - snappedTaskRowWidth = taskWidthAndSpacing; + snappedTaskRowWidth = largeTileRowWidth; + } + if (taskView == expectedCurrentTaskView) { + expectedCurrentTaskRowWidth = largeTileRowWidth; } } else { - if (i > focusedTaskIndex) { - // For tasks after the focused task, shift by focused task's width and spacing. - gridTranslations[i] += - mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; + if (encounteredLastLargeTaskView) { + // For tasks after the last large task, shift by large task's width and spacing. + gridTranslation += + mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing; } else { - // For task before the focused task, accumulate the width and spacing to - // calculate the distance focused task need to shift. - focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; + // For TaskViews before the new focused TaskView, accumulate the width and + // spacing to calculate the distance the new focused TaskView needs to shift. + // This could happen for example after multiple times of dismissing the + // focused TaskView, the triggered rebalance might set a non-first TaskView + // inside `mChildren` as the new focused TaskView. + focusedTaskViewShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; } int taskViewId = taskView.getTaskViewId(); - // Rebalance the grid starting after a certain index boolean isTopRow; - if (isTaskDismissal) { - if (i > startRebalanceAfter) { + if (mAnyTaskHasBeenDismissed) { + // Rebalance the grid starting after a certain index. + if (encounteredLastVisibleTaskView) { mTopRowIdSet.remove(taskViewId); isTopRow = topRowWidth <= bottomRowWidth; } else { @@ -3098,47 +3432,47 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex) { - continue; - } - widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; + for (TaskView bottomTaskView : lastBottomTaskViews) { + widthOffset += bottomTaskView.getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; - gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX; + gridTranslation += topAccumulatedTranslationX + currentTaskTranslationX; topAccumulatedTranslationX += currentTaskTranslationX; + lastTopTaskViews.add(taskView); + lastBottomTaskViews.clear(); } else { bottomRowWidth += taskWidthAndSpacing; - bottomSet.add(i); // Move into bottom row. taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff); // Move horizontally into empty space. float widthOffset = 0; - for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) { - if (j == focusedTaskIndex) { - continue; - } - widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; + for (TaskView topTaskView : lastTopTaskViews) { + widthOffset += topTaskView.getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; - gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX; + gridTranslation += bottomAccumulatedTranslationX + currentTaskTranslationX; bottomAccumulatedTranslationX += currentTaskTranslationX; + lastBottomTaskViews.add(taskView); + lastTopTaskViews.clear(); } + int taskViewRowWidth = isTopRow ? topRowWidth : bottomRowWidth; if (taskView == snappedTaskView) { - snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth; + snappedTaskRowWidth = taskViewRowWidth; + } + if (taskView == expectedCurrentTaskView) { + expectedCurrentTaskRowWidth = taskViewRowWidth; } } + gridTranslations.put(taskView, gridTranslation); } // We need to maintain snapped task's page scroll invariant between quick switch and @@ -3149,22 +3483,22 @@ public abstract class RecentsView 0) { // Shift by focused task's width and spacing if a task is focused. clearAllTotalTranslationX += - mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; + mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing; } // Make sure there are enough space between snapped page and ClearAllButton, for the case @@ -3210,27 +3552,33 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() - .taskSecondaryTranslation.value = runningTask.getGridTranslationY() - ); + if (mAddDesktopButton != null) { + TaskView firstTaskView = getFirstTaskView(); + float translationX = 0f; + if (firstTaskView != null) { + translationX += firstTaskView.getGridTranslationX(); + } + if (focusedTaskViewShift != 0) { + // If the focused task is inserted between `firstTaskView` and + // `mAddDesktopButton`, shift `mAddDesktopButton` to accommodate. + translationX += largeTaskWidthAndSpacing; + } + mAddDesktopButton.setGridTranslationX(translationX); } mClearAllButton.setGridTranslationPrimary( @@ -3238,19 +3586,18 @@ public abstract class RecentsView { if (!mEnableDrawingLiveTile) return; - runActionOnRemoteHandles( - remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() - .taskSecondaryTranslation.value = getPagedOrientationHandler() - .getSecondaryValue(taskView.getTranslationX(), - taskView.getTranslationY() - )); + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator().taskSecondaryTranslation.value = + taskView.getSecondaryDismissTranslationProperty().get(taskView)); redrawLiveTile(); }); } @@ -3400,6 +3738,7 @@ public abstract class RecentsView resetFromSplitSelectionState()))); + firstFloatingTaskView.setContentDescription(splitAnimInitProps.getContentDescription()); // SplitInstructionsView: animate in safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView()); @@ -3435,11 +3774,7 @@ public abstract class RecentsView 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f; + !mTopRowIdSet.isEmpty() && mTopRowIdSet.size() >= (taskCount - 1) / 2f; // Pick the next focused task from the preferred row. - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); - if (taskView == dismissedTaskView) { + for (TaskView taskView : getTaskViews()) { + if (taskView == dismissedTaskView || taskView.isLargeTile()) { continue; } boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId()); @@ -3516,15 +3865,16 @@ public abstract class RecentsView v.getVisibility() != GONE && v != dismissedTaskView); - if (count > 1) { - scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); - } } + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + getPageScrolls(newScroll, false, + v -> v.getVisibility() != GONE && v != dismissedTaskView); + if (count > 1) { + scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); + } + + isSlidingTasks = isStagingFocusedTask || areAllDesktopTasksDismissed; float dismissTranslationInterpolationEnd = 1; boolean closeGapBetweenClearAll = false; boolean isClearAllHidden = isClearAllHidden(); @@ -3535,31 +3885,46 @@ public abstract class RecentsView bottomGridRowSize; + boolean bottomRowLonger = bottomGridRowSize > topGridRowSize; + boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId); + boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed; + if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) { + topGridRowSize--; + } + if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) { + bottomGridRowSize--; + } + int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize) + * (mLastComputedGridTaskSize.width() + mPageSpacing); + if (!enableGridOnlyOverview() && !isStagingFocusedTask) { + longRowWidth += mLastComputedTaskSize.width() + mPageSpacing; + } + // Compensate the removed gap if we don't already have shortTotalCompensation, + // and adjust accordingly to the new shortTotalCompensation after dismiss. + int newClearAllShortTotalWidthTranslation = 0; + if (mClearAllShortTotalWidthTranslation == 0) { + // If first task is not in the expected position (mLastComputedTaskSize) and being too + // close to ClearAllButton, then apply extra translation to ClearAllButton. + int firstTaskStart = mLastComputedGridSize.left + longRowWidth; + int expectedFirstTaskStart = mLastComputedTaskSize.right; + if (firstTaskStart < expectedFirstTaskStart) { + newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart; + } + } + if (lastGridTaskView != null && ( + (!isExpressiveDismiss && lastGridTaskView.isVisibleToUser()) || (isExpressiveDismiss + && (isTaskViewVisible(lastGridTaskView) + || lastGridTaskView == dismissedTaskView)))) { // After dismissal, animate translation of the remaining tasks to fill any gap left // between the end of the grid and the clear all button. Only animate if the clear // all button is visible or would become visible after dismissal. float longGridRowWidthDiff = 0; - int topGridRowSize = mTopRowIdSet.size(); - int bottomGridRowSize = taskCount - mTopRowIdSet.size() - - (enableGridOnlyOverview() ? 0 : 1); - boolean topRowLonger = topGridRowSize > bottomGridRowSize; - boolean bottomRowLonger = bottomGridRowSize > topGridRowSize; - boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId); - boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed; - if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) { - topGridRowSize--; - } - if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) { - bottomGridRowSize--; - } - int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize) - * (mLastComputedGridTaskSize.width() + mPageSpacing); - if (!enableGridOnlyOverview() && !isStagingFocusedTask) { - longRowWidth += mLastComputedTaskSize.width() + mPageSpacing; - } - float gapWidth = 0; if ((topRowLonger && dismissedTaskFromTop) || (bottomRowLonger && dismissedTaskFromBottom)) { @@ -3571,17 +3936,6 @@ public abstract class RecentsView 0) { if (mClearAllShortTotalWidthTranslation == 0) { - // Compensate the removed gap if we don't already have shortTotalCompensation, - // and adjust accordingly to the new shortTotalCompensation after dismiss. - int newClearAllShortTotalWidthTranslation = 0; - if (longRowWidth < mLastComputedGridSize.width()) { - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - newClearAllShortTotalWidthTranslation = - (mIsRtl - ? mLastComputedTaskSize.right - : deviceProfile.widthPx - mLastComputedTaskSize.left) - - longRowWidth - deviceProfile.overviewGridSideMargin; - } float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation; longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation; } @@ -3610,6 +3964,22 @@ public abstract class RecentsView { - runActionOnRemoteHandles( - remoteTargetHandle -> - remoteTargetHandle.getTaskViewSimulator() - .taskPrimaryTranslation.value = - getPagedOrientationHandler().getPrimaryValue( - child.getTranslationX(), - child.getTranslationY() - )); - redrawLiveTile(); - }); + if (!isExpressiveDismiss) { + translateTaskWhenDismissed( + child, + Math.abs(i - dismissedIndex), + scrollDiff, + anim, + splitTimings); + } + if (child instanceof TaskView taskView) { + mTaskViewsDismissPrimaryTranslations.put(taskView, scrollDiffPerPage); } needsCurveUpdates = true; } - } else if (child instanceof TaskView) { - TaskView taskView = (TaskView) child; - if (isFocusedTaskDismissed) { - if (nextFocusedTaskView != null && - !isSameGridRow(taskView, nextFocusedTaskView)) { - continue; - } - } else { - if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) { - continue; - } - } + } else if (child instanceof TaskView taskView) { // Animate task with index >= dismissed index and in the same row as the // dismissed index or next focused index. Offset successive task dismissal // durations for a staggered effect. - distanceFromDismissedTask++; - int staggerColumn = isStagingFocusedTask + int staggerColumn = isSlidingTasks ? (int) Math.ceil(distanceFromDismissedTask / 2f) : distanceFromDismissedTask; // Set timings based on if user is initiating splitscreen on the focused task, // or splitting/dismissing some other task. - float animationStartProgress = isStagingFocusedTask - ? Utilities.boundToRange( - splitTimings.getGridSlideStartOffset() - + (splitTimings.getGridSlideStaggerOffset() - * staggerColumn), - 0f, - dismissTranslationInterpolationEnd) - : Utilities.boundToRange( - INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - * staggerColumn, 0f, dismissTranslationInterpolationEnd); - float animationEndProgress = isStagingFocusedTask - ? Utilities.boundToRange( - splitTimings.getGridSlideStartOffset() - + (splitTimings.getGridSlideStaggerOffset() * staggerColumn) - + splitTimings.getGridSlideDurationOffset(), - 0f, - dismissTranslationInterpolationEnd) - : dismissTranslationInterpolationEnd; - Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR; + final float animationStartProgress; + if (isSlidingTasks) { + float slidingStartOffset = splitTimings.getGridSlideStartOffset() + + (splitTimings.getGridSlideStaggerOffset() * staggerColumn); + if (areAllDesktopTasksDismissed) { + animationStartProgress = Utilities.boundToRange( + slidingStartOffset + + splitTimings.getDesktopFadeSplitAnimationEndOffset(), + 0f, + dismissTranslationInterpolationEnd); + } else { + animationStartProgress = Utilities.boundToRange( + slidingStartOffset, + 0f, + dismissTranslationInterpolationEnd); + } + } else { + animationStartProgress = Utilities.boundToRange( + INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + * staggerColumn, 0f, dismissTranslationInterpolationEnd); + } + final float animationEndProgress; + if (isSlidingTasks && taskView != nextFocusedTaskView) { + animationEndProgress = Utilities.boundToRange( + splitTimings.getGridSlideStartOffset() + + (splitTimings.getGridSlideStaggerOffset() * staggerColumn) + + splitTimings.getGridSlideDurationOffset(), + 0f, + dismissTranslationInterpolationEnd); + } else { + animationEndProgress = dismissTranslationInterpolationEnd; + } + + Interpolator dismissInterpolator = isSlidingTasks ? EMPHASIZED : LINEAR; + + float primaryTranslation = 0; if (taskView == nextFocusedTaskView) { // Enlarge the task to be focused next, and translate into focus position. float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width(); anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale, clampToProgress(LINEAR, animationStartProgress, dismissTranslationInterpolationEnd)); - anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), - mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth, - clampToProgress(LINEAR, animationStartProgress, - dismissTranslationInterpolationEnd)); + primaryTranslation += dismissedTaskWidth; float secondaryTranslation = -mTaskGridVerticalDiff; if (!nextFocusedTaskFromTop) { secondaryTranslation -= mTopBottomRowHeightDiff; @@ -3805,27 +4133,45 @@ public abstract class RecentsView= dismissedIndex && isSameGridRow( + taskView, dismissedTaskView))) { + primaryTranslation += nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth; - if (isStagingFocusedTask) { - // Moves less if focused task is not in scroll position. - int focusedTaskScroll = getScrollForPage(dismissedIndex); - int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this); - int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll; - primaryTranslation += - mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff; - } + } + if (!(taskView instanceof DesktopTaskView)) { + primaryTranslation += mIsRtl ? slidingTranslation : -slidingTranslation; + } - anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), - mIsRtl ? primaryTranslation : -primaryTranslation, - clampToProgress(dismissInterpolator, animationStartProgress, - animationEndProgress)); + if (primaryTranslation != 0) { + float finalTranslation = mIsRtl ? primaryTranslation : -primaryTranslation; + float startTranslation = 0; + if (!(taskView instanceof DesktopTaskView) && slidingTranslation != 0) { + startTranslation = isTaskViewVisible(taskView) ? 0 + : finalTranslation + (mIsRtl ? -mLastComputedTaskSize.right + : mLastComputedTaskSize.right); + } + // Expressive dismiss will animate the translations of taskViews itself. + if (!isExpressiveDismiss) { + Animator dismissAnimator = ObjectAnimator.ofFloat(taskView, + taskView.getPrimaryDismissTranslationProperty(), + startTranslation, finalTranslation); + dismissAnimator.setInterpolator( + clampToProgress(dismissInterpolator, animationStartProgress, + animationEndProgress)); + anim.add(dismissAnimator); + } + mTaskViewsDismissPrimaryTranslations.put(taskView, (int) finalTranslation); + distanceFromDismissedTask++; } } } + if (dismissingForSplitSelection) { + createInitialSplitSelectAnimation(anim); + } if (needsCurveUpdates) { anim.addOnFrameCallback(this::updateCurveProperties); @@ -3834,21 +4180,26 @@ public abstract class RecentsView InteractionJankMonitorWrapper.begin(this, + Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS)); + } mPendingAnimation = anim; final TaskView finalNextFocusedTaskView = nextFocusedTaskView; final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll; final boolean finalSnapToLastTask = snapToLastTask; final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed; - mPendingAnimation.addEndListener(new Consumer() { + mPendingAnimation.addEndListener(new Consumer<>() { @Override public void accept(Boolean success) { - if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) { + if (mEnableDrawingLiveTile && dismissedTaskView != null + && dismissedTaskView.isRunningTask() && success) { finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, - () -> onEnd(success)); + () -> onEnd(true)); } else { onEnd(success); } @@ -3861,17 +4212,16 @@ public abstract class RecentsView removeTaskInternal(dismissedTaskViewId)); + () -> removeTaskInternal(dismissedTaskView)); } else { - removeTaskInternal(dismissedTaskViewId); + removeTaskInternal(dismissedTaskView); } - announceForAccessibility( - getResources().getString(R.string.task_view_closed)); mContainer.getStatsLogManager().logger() - .withItemInfo(dismissedTaskView.getFirstItemInfo()) + .withItemInfo(dismissedTaskView.getItemInfo()) .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); } @@ -3887,7 +4237,7 @@ public abstract class RecentsView= screenEnd - mPageSpacing; } if (shouldRebalance) { - updateGridProperties(/*isTaskDismissal=*/ true, - highestVisibleTaskIndex); + updateGridProperties(highestVisibleTaskView); updateScrollSynchronously(); } } - IntArray topRowIdArray = getTopRowIdArray(); - IntArray bottomRowIdArray = getBottomRowIdArray(); + IntArray topRowIdArray = mUtils.getTopRowIdArray(); + IntArray bottomRowIdArray = mUtils.getBottomRowIdArray(); if (finalSnapToLastTask) { // If snapping to last task, find the last task after dismissal. pageToSnapTo = indexOfChild( getLastGridTaskView(topRowIdArray, bottomRowIdArray)); + + if (pageToSnapTo == INVALID_PAGE) { + // Snap to latest large tile page after dismissing the + // last grid task. This will prevent snapping to page 0 when + // desktop task is visible as large tile. + pageToSnapTo = indexOfChild(mUtils.getLastLargeTaskView()); + } } else if (taskViewIdToSnapTo != -1) { // If snapping to another page due to indices rearranging, find // the new index after dismissal & rearrange using the task view id. @@ -4045,10 +4403,105 @@ public abstract class RecentsView { + runActionOnRemoteHandles( + remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator() + .taskPrimaryTranslation.value = + getPagedOrientationHandler().getPrimaryValue( + view.getTranslationX(), + view.getTranslationY() + )); + redrawLiveTile(); + }); + } + } + /** * Hides all overview actions if user is halfway through split selection, shows otherwise. * We only show split option if: @@ -4060,9 +4513,6 @@ public abstract class RecentsView indexOfChild(bottomTask) ? topTask : bottomTask; + } else if (lastVisibleTaskView != null) { break; } } - return lastVisibleIndex; + return lastVisibleTaskView; } - private void removeTaskInternal(int dismissedTaskViewId) { - int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId); - UI_HELPER_EXECUTOR.getHandler().post( - () -> { - for (int taskId : taskIds) { - if (taskId != -1) { - ActivityManagerWrapper.getInstance().removeTask(taskId); - } - } - }); + private void removeTaskInternal(@NonNull TaskView dismissedTaskView) { + UI_HELPER_EXECUTOR + .getHandler() + .post( + () -> { + if (dismissedTaskView instanceof DesktopTaskView desktopTaskView) { + removeDesktopTaskView(desktopTaskView); + } else { + for (int taskId : dismissedTaskView.getTaskIds()) { + ActivityManagerWrapper.getInstance().removeTask(taskId); + } + } + }); + } + + private void removeDesktopTaskView(DesktopTaskView desktopTaskView) { + if (areMultiDesksFlagsEnabled()) { + SystemUiProxy.INSTANCE + .get(getContext()) + .removeDesk(desktopTaskView.getDeskId()); + } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + SystemUiProxy.INSTANCE + .get(getContext()) + .removeDefaultDeskInDisplay( + mContainer.getDisplay().getDisplayId()); + } } protected void onDismissAnimationEnds() { @@ -4167,19 +4597,24 @@ public abstract class RecentsView { if (isSuccess) { + // Remove desktops first, since desks can be empty (so they have no recent tasks), + // and closing all tasks on a desk doesn't always necessarily mean that the desk + // will be removed. So, there are no guarantees that the below call to + // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough. + SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks(); + // Remove all the task views now finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> { UI_HELPER_EXECUTOR.getHandler().post( ActivityManagerWrapper.getInstance()::removeAllRecentTasks); - removeTasksViewsAndClearAllButton(); + removeAllTaskViews(); startHome(); }); } @@ -4189,7 +4624,7 @@ public abstract class RecentsView onEndRunnable) { + PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION); + createTaskDismissAnimation(pa, taskView, false /* animateTaskView */, true /* removeTask */, + DISMISS_TASK_DURATION, false /* dismissingForSplitSelection*/, + true /* isExpressiveDismiss */); + pa.addEndListener((success) -> onEndRunnable.invoke()); runDismissAnimation(pa); } @@ -4268,27 +4733,42 @@ public abstract class RecentsView= 0; i--) { - TaskView child = requireTaskViewAt(i); - if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) { - continue; - } - child.setStableAlpha(alpha); + for (TaskView taskView : getTaskViews()) { + taskView.setStableAlpha(alpha); } mClearAllButton.setContentAlpha(mContentAlpha); + + if (mAddDesktopButton != null) { + mAddDesktopButton.setContentAlpha(mContentAlpha); + } int alphaInt = Math.round(alpha * 255); mEmptyMessagePaint.setAlpha(alphaInt); mEmptyIcon.setAlpha(alphaInt); @@ -4396,6 +4875,12 @@ public abstract class RecentsView + remoteTargetHandle.getTaskViewSimulator().setPivotOverride(mTempPointF)); + } } /** @@ -4543,10 +5032,15 @@ public abstract class RecentsView 0; - if (isModalGridWithoutFocusedTask) { - modalMidpoint = indexOfChild(mSelectedTask); + TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask + : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true, + /*runningTaskView=*/null); + int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask); + boolean shouldCalculateOffsetForAllTasks = showAsGrid + && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile()) + && mTaskModalness > 0; + if (shouldCalculateOffsetForAllTasks) { + modalMidpoint = indexOfChild(getSelectedTaskView()); } float midpointOffsetSize = 0; @@ -4561,6 +5055,7 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() @@ -4606,11 +5126,11 @@ public abstract class RecentsView translationPropertyY = + taskView.getSecondaryTaskOffsetTranslationProperty(); + translationPropertyY.set(taskView, totalTranslationY); } } updateCurveProperties(); @@ -4642,6 +5162,7 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() @@ -4756,23 +5284,21 @@ public abstract class RecentsView taskContainer.getOverlay().resetModalVisuals()); } } @@ -4794,22 +5320,23 @@ public abstract class RecentsView { + if (taskView instanceof DesktopTaskView) { + // Setting pivot to scale down from screen centre. + if (isTaskViewVisible(taskView)) { + float pivotX = 0f; + if (index < mCurrentPage) { + pivotX = mIsRtl ? taskView.getWidth() / 2f - mPageSpacing + - taskView.getWidth() + : taskView.getWidth() / 2f + mPageSpacing + taskView.getWidth(); + } else if (index == mCurrentPage) { + pivotX = taskView.getWidth() / 2f; + } else { + pivotX = mIsRtl ? taskView.getWidth() + mPageSpacing + + taskView.getWidth() + : taskView.getWidth() - mPageSpacing - taskView.getWidth(); + } + taskView.setPivotX(pivotX); + taskView.setPivotY(taskView.getHeight() / 2f); + builder.add(ObjectAnimator + .ofFloat(taskView, TaskView.DISMISS_SCALE, 0.95f), + clampToProgress(timings.getDesktopTaskScaleInterpolator(), 0f, + timings.getDesktopFadeSplitAnimationEndOffset())); + } + builder.addFloat(taskView, SPLIT_ALPHA, 1f, 0f, + clampToProgress(deskTopFadeInterPolator, 0f, + timings.getDesktopFadeSplitAnimationEndOffset())); + } + }); + } + } + + /** + * While exiting from split mode, show all existing DesktopTaskViews. + */ + public void resetDesktopTaskFromSplitSelectState() { + if (enableLargeDesktopWindowingTile()) { + for (TaskView taskView : getTaskViews()) { + if (taskView instanceof DesktopTaskView) { + taskView.setSplitAlpha(1f); + } + } } } @@ -4849,32 +5423,46 @@ public abstract class RecentsView{ - thumbnail.refreshSplashView(); - mSplitHiddenTaskView.updateSnapshotRadius(); + builder.addOnFrameCallback(() -> { + if (!enableRefactorTaskThumbnail()) { + taskContainer.getThumbnailViewDeprecated().refreshSplashView(); + } + mSplitHiddenTaskView.updateFullscreenParams(); }); } else if (isInitiatingSplitFromTaskView) { + if (Flags.enableHoverOfChildElementsInTaskview()) { + mSplitHiddenTaskView.setBorderEnabled(false); + } // Splitting from Overview for fullscreen task createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, - true /* dismissingForSplitSelection*/); + true /* dismissingForSplitSelection*/, false /* isExpressiveDismiss */); } else { // Splitting from Home - createInitialSplitSelectAnimation(builder); + TaskView currentPageTaskView = getTaskViewAt(mCurrentPage); + // When current page is a Desktop task it needs special handling to + // display correct animation in split mode + if (currentPageTaskView instanceof DesktopTaskView) { + createTaskDismissAnimation(builder, null, true, false, duration, + true /* dismissingForSplitSelection*/, false /* isExpressiveDismiss */); + } else { + createInitialSplitSelectAnimation(builder); + } } } @@ -4885,17 +5473,21 @@ public abstract class RecentsView { mSplitSelectStateController.launchSplitTasks( aBoolean1 -> { - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.resetState(); - } else { - resetFromSplitSelectionState(); - } InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER); + mSplitSelectStateController.resetState(); }); }); @@ -4983,7 +5571,7 @@ public abstract class RecentsView, FloatProperty> taskViewsFloat = + Pair>, FloatProperty>> taskViewsFloat = orientationHandler.getSplitSelectTaskOffset( TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, mContainer.getDeviceProfile()); @@ -5110,15 +5696,8 @@ public abstract class RecentsView 0) { - final View taskView = requireTaskViewAt(0); - requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); - mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), - taskView.getBottom()); - } + mUtils.updateTaskViewDeadZoneRect(mTaskViewDeadZoneRect, mTopRowDeadZoneRect, + mBottomRowDeadZoneRect); } private void updateEmptyStateUi(boolean sizeChanged) { @@ -5131,7 +5710,7 @@ public abstract class RecentsView + remoteTargetHandle.getTaskViewSimulator() + .setPivotOverride(mTempPointF)); + mBlurUtils.setDrawLiveTileBelowRecents(false); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + // If live tile is not launching, reset the pivot applied above. + if (!taskView.isRunningTask()) { + runActionOnRemoteHandles( + remoteTargetHandle -> { + remoteTargetHandle.getTaskViewSimulator().setPivotOverride( + null); + }); + } + } + }); } else if (!showAsGrid) { // We are launching an adjacent task, so parallax the center and other adjacent task. - float displacementX = tv.getWidth() * (toScale - 1f); + float displacementX = taskView.getWidth() * (toScale - 1f); float primaryTranslation = mIsRtl ? -displacementX : displacementX; anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation)); @@ -5217,6 +5829,20 @@ public abstract class RecentsView { + int targetSysUiFlags = taskView.getSysUiStatusNavFlags(); + final boolean[] passedOverviewThreshold = new boolean[]{false}; + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(taskView); + anim.play(new AnimatedFloat(v -> { // Once we pass a certain threshold, update the sysui flags to match the target // tasks' flags - if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) { + if (v > UPDATE_SYSUI_FLAGS_THRESHOLD) { mContainer.getSystemUiController().updateUiState( UI_STATE_FULLSCREEN_TASK, targetSysUiFlags); } else { @@ -5271,8 +5894,7 @@ public abstract class RecentsView= - SUCCESS_TRANSITION_PROGRESS; + final boolean passed = v >= SUCCESS_TRANSITION_PROGRESS; if (passed != passedOverviewThreshold[0]) { passedOverviewThreshold[0] = passed; performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, @@ -5282,30 +5904,26 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() - .addOverviewToAppAnim(mPendingAnimation, interpolator)); - mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); + if (taskView.isRunningTask()) { + runActionOnRemoteHandles( + remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() + .addOverviewToAppAnim(mPendingAnimation, interpolator)); + mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); + } + mPendingAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mBlurUtils.setDrawLiveTileBelowRecents(false); + } + }); mPendingAnimation.addEndListener(isSuccess -> { if (isSuccess) { - if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds()) + if (taskView instanceof GroupedTaskView && hasAllValidTaskIds(taskView.getTaskIds()) && mRemoteTargetHandles != null) { // TODO(b/194414938): make this part of the animations instead. TaskViewUtils.createSplitAuxiliarySurfacesAnimator( @@ -5315,13 +5933,13 @@ public abstract class RecentsView outChildren) { - // Add children in reverse order - for (int i = getChildCount() - 1; i >= 0; --i) { - outChildren.add(getChildAt(i)); - } + outChildren.addAll(getAccessibilityChildren()); + } + + public List getAccessibilityChildren() { + return mUtils.getAccessibilityChildren(); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); final AccessibilityNodeInfo.CollectionInfo - collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( - 1, getTaskViewCount(), false, - AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); + collectionInfo = new AccessibilityNodeInfo.CollectionInfo( + 1, getAccessibilityChildren().size(), false); info.setCollectionInfo(collectionInfo); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - - final int taskViewCount = getTaskViewCount(); - event.setScrollable(taskViewCount > 0); + event.setScrollable(hasTaskViews()); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + final List accessibilityChildren = getAccessibilityChildren(); final int[] visibleTasks = getVisibleChildrenRange(); - event.setFromIndex(taskViewCount - visibleTasks[1]); - event.setToIndex(taskViewCount - visibleTasks[0]); - event.setItemCount(taskViewCount); + event.setFromIndex(accessibilityChildren.indexOf(getChildAt(visibleTasks[1]))); + event.setToIndex(accessibilityChildren.indexOf(getChildAt(visibleTasks[0]))); + event.setItemCount(accessibilityChildren.size()); } } @@ -5399,6 +6025,10 @@ public abstract class RecentsView { TransformParams params = remoteTargetHandle.getTransformParams(); @@ -5408,6 +6038,7 @@ public abstract class RecentsView { final TransformParams params = remoteTargetHandle.getTransformParams(); + if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) { + params.setHomeBuilderProxy((builder, app, transformParams) -> { + mTmpMatrix.setScale( + 1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY()); + builder.setMatrix(mTmpMatrix).setAlpha(1f).setShow(); + }); + } + if (mSyncTransactionApplier != null) { params.setSyncTransactionApplier(mSyncTransactionApplier); params.getTargetSet().addReleaseCheck(mSyncTransactionApplier); @@ -5480,12 +6120,19 @@ public abstract class RecentsView { + mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> { if (onFinishComplete != null) { onFinishComplete.run(); } @@ -5544,6 +6192,9 @@ public abstract class RecentsView 0); + } + return indexOfChild(firstView); } private int getLastViewIndex() { + final View lastView; if (!mDisallowScrollToClearAll) { - return indexOfChild(mClearAllButton); + // When ClearAllButton is present, it always end with ClearAllButton. + lastView = mClearAllButton; + } else if (mShowAsGridLastOnLayout) { + // When ClearAllButton is absent, for the grid Overview, it always end with a grid task + // if they exist, otherwise it ends with a large tile (focused task or desktop task). + TaskView lastGridTaskView = getLastGridTaskView(); + if (lastGridTaskView != null) { + lastView = lastGridTaskView; + } else { + lastView = mUtils.getLastLargeTaskView(); + } + } else { + lastView = mUtils.getLastTaskViewInCarousel( + /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0); } - - if (!mShowAsGridLastOnLayout) { - return getTaskViewCount() - 1; - } - - TaskView lastGridTaskView = getLastGridTaskView(); - if (lastGridTaskView != null) { - return indexOfChild(lastGridTaskView); - } - - // Returns focus task if there are no grid tasks. - return indexOfChild(getFocusedTaskView()); + return indexOfChild(lastView); } /** @@ -5652,42 +6330,55 @@ public abstract class RecentsView { float scrollDiff = taskView.getScrollAdjustment(showAsGrid); - int pageScroll = newPageScrolls[i] + Math.round(scrollDiff); + int pageScroll = newPageScrolls[index] + Math.round(scrollDiff); if ((mIsRtl && pageScroll < lastTaskScroll) || (!mIsRtl && pageScroll > lastTaskScroll)) { pageScroll = lastTaskScroll; } - if (outPageScrolls[i] != pageScroll) { - pageScrollChanged = true; - outPageScrolls[i] = pageScroll; - } + outPageScrolls[index] = pageScroll; if (DEBUG) { - Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]); + Log.d(TAG, + "getPageScrolls - outPageScrolls[" + index + "]: " + outPageScrolls[index]); + } + }); + + int addDesktopButtonIndex = indexOfChild(mAddDesktopButton); + if (addDesktopButtonIndex >= 0 && addDesktopButtonIndex < outPageScrolls.length) { + int firstViewIndex = getFirstViewIndex(); + if (firstViewIndex >= 0 && firstViewIndex < outPageScrolls.length) { + // If we can scroll to [AddDesktopButton], make its page scroll equal to + // the first [TaskView]. Otherwise, make its page scroll out of range of + // [minScroll, maxScroll]. + if (!mDisallowScrollToAddDesk) { + outPageScrolls[addDesktopButtonIndex] = outPageScrolls[firstViewIndex]; + } else { + outPageScrolls[addDesktopButtonIndex] = + outPageScrolls[firstViewIndex] + (mIsRtl ? 1 : -1); + } + } + + if (DEBUG) { + Log.d(TAG, "getPageScrolls - addDesktopButtonScroll: " + + outPageScrolls[addDesktopButtonIndex]); } } if (DEBUG) { Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll); } - return pageScrollChanged; + return !Arrays.equals(oldPageScrolls, outPageScrolls); } @Override @@ -5704,12 +6395,12 @@ public abstract class RecentsView 0 - ? (int) Utilities.mapRange( - mAdjacentPageHorizontalOffset, - getOverScrollShift(), - getUndampedOverScrollShift()) - : getOverScrollShift(); + ? (int) Utilities.mapRange( + mAdjacentPageHorizontalOffset, + getOverScrollShift(), + getUndampedOverScrollShift()) + : getOverScrollShift(); return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this) + overScrollShift + getOffsetFromScrollPosition(pageIndex); } @@ -5786,7 +6483,8 @@ public abstract class RecentsView getEventDispatcher(float navbarRotation) { @@ -5874,19 +6572,27 @@ public abstract class RecentsView fullyVisibleTaskIds = new HashSet<>(); + for (TaskView taskView : getTaskViews()) { + if (isTaskViewFullyVisible(taskView)) { + fullyVisibleTaskIds.addAll(taskView.getTaskIdSet()); + } + } + mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds); + } else { + TaskView focusedTaskView = getFocusedTaskView(); + for (TaskView taskView : getTaskViews()) { + if (taskView == focusedTaskView) { + continue; + } + taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView)); + } + // Focus task overlay should be enabled and refreshed at last + if (focusedTaskView != null) { + focusedTaskView.setOverlayEnabled( + mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView)); } - taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView)); - } - // Focus task overlay should be enabled and refreshed at last - if (focusedTaskView != null) { - focusedTaskView.setOverlayEnabled( - mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView)); } } @@ -5894,6 +6600,10 @@ public abstract class RecentsView updatedThumbnails = mUtils.screenshotTasks(taskView); + if (enableRefactorTaskThumbnail()) { + mHelper.switchToScreenshot(taskView, updatedThumbnails, onFinishRunnable); + } else { + setRunningTaskViewShowScreenshot(true, updatedThumbnails); + ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } - ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } /** @@ -5980,9 +6677,12 @@ public abstract class RecentsView getContainerInterface(int displayId); + /** * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the * tasks to be dimmed while other elements in the recents view are left alone. @@ -6047,12 +6759,11 @@ public abstract class RecentsView()), + mOrientationState, mTaskOverlayFactory); + + Objects.requireNonNull(mAddDesktopButton); + final int insertionIndex = 1 + indexOfChild(mAddDesktopButton); + addView(desktopTaskView, insertionIndex); + + updateTaskSize(); + mUtils.updateChildTaskOrientations(); + updateScrollSynchronously(); + + // Set Current Page based on the stored TaskView. + if (currentTaskView != null) { + setCurrentPage(indexOfChild(currentTaskView)); + } + } + + @Override + public void onDeskRemoved(int displayId, int deskId) { + // Ignore desk changes that don't belong to this display. + if (displayId != mContainer.getDisplay().getDisplayId()) { + return; + } + + // We need to distinguish between desk removals that are triggered from outside of overview + // vs. the ones that were initiated from overview by dismissing the corresponding desktop + // task view. + var taskView = mUtils.getDesktopTaskViewForDeskId(deskId); + if (taskView != null) { + dismissTaskView(taskView, true, true); + } + } + + @Override + public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) { + // TODO: b/400870600 - We may need to add code here to special case when an empty desk gets + // activated, since `RemoteDesktopLaunchTransitionRunner` doesn't always get triggered. + } + /** Get the color used for foreground scrimming the RecentsView for sharing. */ public static int getForegroundScrimDimColor(Context context) { return context.getColor(R.color.overview_foreground_scrim_color); @@ -6337,11 +7110,31 @@ public abstract class RecentsView finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, + () -> moveTaskToDesktopInternal(taskContainer, successCallback))); + } + + private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) { + if (mDesktopRecentsTransitionController == null) { + return; + } + mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id); successCallback.run(); } + // Logs when the orientation of Overview changes. We log both real and fake orientation changes. private void logOrientationChanged() { // Only log when Overview is showing. @@ -6360,7 +7153,47 @@ public abstract class RecentsViewWhen a task dismiss is cancelled, the task will return to its original position via a + * spring animation. As it passes the threshold of its settling state, its neighbors will + * spring in response to the perceived impact of the settling task. + */ + public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView, + float velocity, boolean isDismissing, int dismissLength, + Function0 onEndRunnable) { + return mDismissUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity, + isDismissing, dismissLength, onEndRunnable); + } + + /** + * Animates RecentsView's scale to the provided value, using spring animations. + */ + public SpringAnimation animateRecentsScale(float scale) { + return mDismissUtils.animateRecentsScale(scale); + } + public interface TaskLaunchListener { void onTaskLaunched(); } + + /** + * Sets whether the remote animation targets should draw below the recents view. + * + * @param drawBelowRecents whether the surface should draw below Recents. + * @param remoteTargetHandles collection of remoteTargetHandles in Recents. + */ + public void setDrawBelowRecents(boolean drawBelowRecents, + RemoteTargetHandle[] remoteTargetHandles) { + mBlurUtils.setDrawBelowRecents(drawBelowRecents, remoteTargetHandles); + } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java index 060c71e44..e61d40269 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java +++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java @@ -16,7 +16,6 @@ package com.android.quickstep.views; -import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.LocusId; @@ -26,9 +25,11 @@ import android.view.MotionEvent; import android.view.View; import android.view.Window; +import androidx.annotation.Nullable; + import com.android.launcher3.BaseActivity; import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.ScrimView; @@ -41,7 +42,7 @@ public interface RecentsViewContainer extends ActivityContext { * Returns an instance of an implementation of RecentsViewContainer * @param context will find instance of recentsViewContainer from given context. */ - static T containerFromContext(Context context) { + static T containerFromContext(Context context) { if (context instanceof RecentsViewContainer) { return (T) context; } else if (context instanceof ContextWrapper) { @@ -51,11 +52,6 @@ public interface RecentsViewContainer extends ActivityContext { } } - /** - * Returns {@link SystemUiController} to manage various window flags to control system UI. - */ - SystemUiController getSystemUiController(); - /** * Returns {@link ScrimView} */ @@ -66,19 +62,6 @@ public interface RecentsViewContainer extends ActivityContext { */ T getOverviewPanel(); - /** - * Returns the RootView - */ - View getRootView(); - - /** - * Dispatches a generic motion event to the view hierarchy. - * Returns the current RecentsViewContainer as context - */ - default Context asContext() { - return (Context) this; - } - /** * @see Window.Callback#dispatchGenericMotionEvent(MotionEvent) */ @@ -92,7 +75,7 @@ public interface RecentsViewContainer extends ActivityContext { /** * Returns overview actions view as a view */ - View getActionsView(); + OverviewActionsView getActionsView(); /** * @see BaseActivity#addForceInvisibleFlag(int) @@ -139,12 +122,6 @@ public interface RecentsViewContainer extends ActivityContext { */ void runOnBindToTouchInteractionService(Runnable r); - /** - * @see Activity#getWindow() - * @return Window - */ - Window getWindow(); - /** * @see * BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener) @@ -173,6 +150,25 @@ public interface RecentsViewContainer extends ActivityContext { */ boolean isRecentsViewVisible(); + /** + * Begins transition to start home through container + */ + default void startHome(){ + // no op + } + + /** + * Checks container to see if we can start home transition safely + */ + boolean canStartHomeSafely(); + + + /** + * Enter staged split directly from the current running app. + * @param leftOrTop if the staged split will be positioned left or top. + */ + default void enterStageSplitFromRunningApp(boolean leftOrTop){} + /** * Overwrites any logged item in Launcher that doesn't have a container with the * {@link com.android.launcher3.touch.PagedOrientationHandler} in use for Overview. @@ -198,4 +194,8 @@ public interface RecentsViewContainer extends ActivityContext { .setOrientationHandler(orientationForLogging)) .build()); } + + void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController); + + @Nullable TaskbarUIController getTaskbarUIController(); } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt new file mode 100644 index 000000000..ff711da70 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt @@ -0,0 +1,55 @@ +/* + * 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.quickstep.views + +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.ViewUtils +import com.android.quickstep.recents.viewmodel.RecentsViewModel +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** Helper for [RecentsView] to interact with the [RecentsViewModel]. */ +class RecentsViewModelHelper( + private val recentsViewModel: RecentsViewModel, + private val recentsCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) { + fun onDestroy() { + recentsCoroutineScope.cancel("RecentsView is being destroyed") + } + + fun switchToScreenshot( + taskView: TaskView, + updatedThumbnails: Map?, + onFinishRunnable: Runnable, + ) { + // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside + // viewAttachedScope. + recentsViewModel.setRunningTaskShowScreenshot(true) + recentsCoroutineScope.launch(dispatcherProvider.background) { + recentsViewModel.waitForRunningTaskShowScreenshotToUpdate() + recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails) + withContext(Dispatchers.Main.immediate) { + ViewUtils.postFrameDrawn(taskView, onFinishRunnable) + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt new file mode 100644 index 000000000..b265b1339 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt @@ -0,0 +1,497 @@ +/* + * 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.quickstep.views + +import android.graphics.PointF +import android.graphics.Rect +import android.util.FloatProperty +import android.view.KeyEvent +import android.view.View +import android.view.View.LAYOUT_DIRECTION_LTR +import android.view.View.LAYOUT_DIRECTION_RTL +import androidx.core.view.children +import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU +import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType +import com.android.launcher3.Flags.enableGridOnlyOverview +import com.android.launcher3.Flags.enableLargeDesktopWindowingTile +import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks +import com.android.launcher3.Utilities.getPivotsForScalingRectToRect +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID +import com.android.launcher3.util.IntArray +import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.isExternalDisplay +import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.wm.shell.shared.GroupedTaskInfo +import java.util.function.BiConsumer +import kotlin.math.min +import kotlin.reflect.KMutableProperty1 + +/** + * Helper class for [RecentsView]. This util class contains refactored and extracted functions from + * RecentsView to facilitate the implementation of unit tests. + */ +class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { + val taskViews = TaskViewsIterable(recentsView) + + /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */ + fun screenshotTasks(taskView: TaskView): Map { + val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap() + return taskView.taskContainers.associate { + it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id) + } + } + + /** + * Sorts task groups to move desktop tasks to the end of the list. + * + * @param tasks List of group tasks to be sorted. + * @return Sorted list of GroupTasks to be used in the RecentsView. + */ + fun sortDesktopTasksToFront(tasks: List): List { + var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP } + if (areMultiDesksFlagsEnabled()) { + // Desk IDs of newer desks are larger than those of older desks, hence we can use them + // to sort desks from old to new. + desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId } + } + return otherTasks + desktopTasks + } + + fun sortExternalDisplayTasksToFront(tasks: List): List { + val (externalDisplayTasks, otherTasks) = + tasks.partition { it.tasks.firstOrNull().isExternalDisplay } + return otherTasks + externalDisplayTasks + } + + class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable { + /** Iterates TaskViews when its index inside the RecentsView is needed. */ + fun forEachWithIndexInParent(consumer: BiConsumer) { + recentsView.children.forEachIndexed { index, child -> + (child as? TaskView)?.let { consumer.accept(index, it) } + } + } + + override fun iterator(): Iterator = + recentsView.children.mapNotNull { it as? TaskView }.iterator() + } + + /** Counts [TaskView]s that are [DesktopTaskView] instances. */ + private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView } + + /** Counts [TaskView]s that are not [DesktopTaskView] instances. */ + fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView } + + /** Returns a list of all large TaskView Ids from [TaskView]s */ + fun getLargeTaskViewIds(): List = taskViews.filter { it.isLargeTile }.map { it.taskViewId } + + /** Returns a list of all large TaskViews [TaskView]s */ + fun getLargeTaskViews(): List = taskViews.filter { it.isLargeTile } + + /** Returns all the TaskViews in the top row, without the focused task */ + fun getTopRowTaskViews(): List = + taskViews.filter { recentsView.mTopRowIdSet.contains(it.taskViewId) } + + /** Returns all the task Ids in the top row, without the focused task */ + fun getTopRowIdArray(): IntArray = getTopRowTaskViews().map { it.taskViewId }.toIntArray() + + /** Returns all the TaskViews in the bottom row, without the focused task */ + fun getBottomRowTaskViews(): List = + taskViews.filter { !recentsView.mTopRowIdSet.contains(it.taskViewId) && !it.isLargeTile } + + /** Returns all the task Ids in the bottom row, without the focused task */ + fun getBottomRowIdArray(): IntArray = getBottomRowTaskViews().map { it.taskViewId }.toIntArray() + + private fun List.toIntArray() = IntArray(size).apply { this@toIntArray.forEach(::add) } + + /** Counts [TaskView]s that are large tiles. */ + fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile } + + /** Counts [TaskView]s that are grid tasks. */ + fun getGridTaskCount(): Int = taskViews.count { it.isGridTask } + + /** Returns the first TaskView that should be displayed as a large tile. */ + fun getFirstLargeTaskView(): TaskView? = + taskViews.firstOrNull { + it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView) + } + + /** + * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist. + */ + fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? { + if (deskId == INACTIVE_DESK_ID) { + return null + } + return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId } + as? DesktopTaskView + } + + /** Returns the active desk ID of the display that contains the [recentsView] instance. */ + fun getActiveDeskIdOnThisDisplay(): Int = + DesktopVisibilityController.INSTANCE.get(recentsView.context) + .getActiveDeskId(recentsView.mContainer.display.displayId) + + /** Returns the expected focus task. */ + fun getFirstNonDesktopTaskView(): TaskView? = + if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView } + else taskViews.firstOrNull() + + /** + * Returns the [TaskView] that should be the current page during task binding, in the following + * priorities: + * 1. Running task + * 2. Focused task + * 3. First non-desktop task + * 4. Last desktop task + * 5. null otherwise + */ + fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? = + runningTaskView + ?: focusedTaskView + ?: taskViews.firstOrNull { + it !is DesktopTaskView && + !(enableSeparateExternalDisplayTasks() && it.isExternalDisplay) + } + ?: taskViews.lastOrNull() + + private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile + + fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int { + if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) { + // Use the [deskId] to keep desks in the order of their creation, as a newer desk + // always has a larger [deskId] than the older desks. + val desktopTaskView = + taskViews.firstOrNull { + it is DesktopTaskView && + it.deskId != INACTIVE_DESK_ID && + it.deskId <= runningTaskView.deskId + } + if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView) + } + val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView()) + return if (getDeviceProfile().isTablet) { + var index = firstTaskViewIndex + if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) { + // For fullsreen tasks, skip over Desktop tasks in its section + index += + if (enableSeparateExternalDisplayTasks()) { + if (runningTaskView.isExternalDisplay) { + taskViews.count { it is DesktopTaskView && it.isExternalDisplay } + } else { + taskViews.count { it is DesktopTaskView && !it.isExternalDisplay } + } + } else { + getDesktopTaskViewCount() + } + } + if (enableSeparateExternalDisplayTasks() && !runningTaskView.isExternalDisplay) { + // For main display section, skip over external display tasks + index += taskViews.count { it.isExternalDisplay } + } + index + } else { + val currentIndex: Int = recentsView.indexOfChild(runningTaskView) + return if (currentIndex != -1) { + currentIndex // Keep the position if running task already in layout. + } else { + // New running task are added to the front to begin with. + firstTaskViewIndex + } + } + } + + /** Returns the first TaskView if it exists, or null otherwise. */ + fun getFirstTaskView(): TaskView? = taskViews.firstOrNull() + + /** Returns the last TaskView if it exists, or null otherwise. */ + fun getLastTaskView(): TaskView? = taskViews.lastOrNull() + + /** Returns the first TaskView that is not large */ + fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile } + + /** Returns the last TaskView that should be displayed as a large tile. */ + fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile } + + /** + * Gets the list of accessibility children. Currently all the children of RecentsViews are + * added, and in the reverse order to the list. + */ + fun getAccessibilityChildren(): List = recentsView.children.toList().reversed() + + @JvmOverloads + /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */ + fun getFirstTaskViewInCarousel( + nonRunningTaskCarouselHidden: Boolean, + runningTaskView: TaskView? = recentsView.runningTaskView, + ): TaskView? = + taskViews.firstOrNull { + it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden) + } + + /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */ + fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? = + taskViews.lastOrNull { + it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden) + } + + /** Returns if any small tasks are fully visible */ + fun isAnySmallTaskFullyVisible(): Boolean = + taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) } + + /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */ + fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) { + taskViews.forEach { taskView -> + taskView.attachAlpha = + if (taskView == recentsView.runningTaskView) { + RUNNING_TASK_ATTACH_ALPHA.get(recentsView) + } else { + if ( + taskView.isVisibleInCarousel( + recentsView.runningTaskView, + nonRunningTaskCarouselHidden, + ) + ) + 1f + else 0f + } + } + } + + fun TaskView.isVisibleInCarousel( + runningTaskView: TaskView?, + nonRunningTaskCarouselHidden: Boolean, + ): Boolean = + if (!nonRunningTaskCarouselHidden) true + else getCarouselType() == runningTaskView.getCarouselType() + + /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */ + private fun TaskView?.getCarouselType(): TaskViewCarousel = + if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN + + private enum class TaskViewCarousel { + FULL_SCREEN, + DESKTOP, + } + + /** Returns true if there are at least one TaskView has been added to the RecentsView. */ + fun hasTaskViews() = taskViews.any() + + fun getTaskContainerById(taskId: Int) = + taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) } + + private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) { + outRowRect.setEmpty() + firstView?.let { + it.getHitRect(TEMP_RECT) + outRowRect.union(TEMP_RECT) + } + lastView?.let { + it.getHitRect(TEMP_RECT) + outRowRect.union(TEMP_RECT) + } + } + + private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) { + if (rowTaskViewIds.isEmpty) { + outRowRect.setEmpty() + return + } + getRowRect( + recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)), + recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)), + outRowRect, + ) + } + + fun updateTaskViewDeadZoneRect( + outTaskViewRowRect: Rect, + outTopRowRect: Rect, + outBottomRowRect: Rect, + ) { + if (!getDeviceProfile().isTablet) { + getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect) + return + } + getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect) + getRowRect(getTopRowIdArray(), outTopRowRect) + getRowRect(getBottomRowIdArray(), outBottomRowRect) + + // Expand large tile Rect to include space between top/bottom row. + val nonEmptyRowRect = + when { + !outTopRowRect.isEmpty -> outTopRowRect + !outBottomRowRect.isEmpty -> outBottomRowRect + else -> return + } + if (recentsView.isRtl) { + if (outTaskViewRowRect.left > nonEmptyRowRect.right) { + outTaskViewRowRect.left = nonEmptyRowRect.right + } + } else { + if (outTaskViewRowRect.right < nonEmptyRowRect.left) { + outTaskViewRowRect.right = nonEmptyRowRect.left + } + } + + // Expand the shorter row Rect to include the space between the 2 rows. + if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return + if (outTopRowRect.width() <= outBottomRowRect.width()) { + if (outTopRowRect.bottom < outBottomRowRect.top) { + outTopRowRect.bottom = outBottomRowRect.top + } + } else { + if (outBottomRowRect.top > outTopRowRect.bottom) { + outBottomRowRect.top = outTopRowRect.bottom + } + } + } + + private fun getTaskMenu(): TaskMenuView? = + getTopOpenViewWithType(recentsView.mContainer, TYPE_TASK_MENU) as? TaskMenuView + + fun shouldInterceptKeyEvent(event: KeyEvent): Boolean { + if (enableOverviewIconMenu()) { + return getTaskMenu()?.isOpen == true || event.keyCode == KeyEvent.KEYCODE_TAB + } + return false + } + + fun updateChildTaskOrientations() { + with(recentsView) { + taskViews.forEach { it.setOrientationState(mOrientationState) } + if (enableOverviewIconMenu()) { + children.forEach { + it.layoutDirection = if (isRtl) LAYOUT_DIRECTION_LTR else LAYOUT_DIRECTION_RTL + } + } + + // Return when it's not fake landscape + if (mOrientationState.isRecentsActivityRotationAllowed) return@with + + // Rotation is supported on phone (details at b/254198019#comment4) + getTaskMenu()?.onRotationChanged() + } + } + + fun updateCentralTask() { + val isTablet: Boolean = getDeviceProfile().isTablet + val actionsViewCanRelateToTaskView = !(isTablet && enableGridOnlyOverview()) + val focusedTaskView = recentsView.focusedTaskView + val currentPageTaskView = recentsView.currentPageTaskView + + fun isInExpectedScrollPosition(taskView: TaskView?) = + taskView?.let { recentsView.isTaskInExpectedScrollPosition(it) } ?: false + + val centralTaskIds: Set = + when { + !actionsViewCanRelateToTaskView -> emptySet() + isTablet && isInExpectedScrollPosition(focusedTaskView) -> + focusedTaskView!!.taskIdSet + isInExpectedScrollPosition(currentPageTaskView) -> currentPageTaskView!!.taskIdSet + else -> emptySet() + } + + recentsView.mRecentsViewModel.updateCentralTaskIds(centralTaskIds) + } + + var deskExplodeProgress: Float = 0f + set(value) { + field = value + taskViews.filterIsInstance().forEach { it.explodeProgress = field } + } + + var selectedTaskView: TaskView? = null + set(newValue) { + val oldValue = field + field = newValue + if (oldValue != newValue) { + onSelectedTaskViewUpdated(oldValue, newValue) + } + } + + private fun onSelectedTaskViewUpdated( + oldSelectedTaskView: TaskView?, + newSelectedTaskView: TaskView?, + ) { + if (!enableGridOnlyOverview()) return + with(recentsView) { + oldSelectedTaskView?.modalScale = 1f + oldSelectedTaskView?.modalPivot = null + + if (newSelectedTaskView == null) return + + val modalTaskBounds = mTempRect + getModalTaskSize(modalTaskBounds) + val selectedTaskBounds = getTaskBounds(newSelectedTaskView) + + // Map bounds to selectedTaskView's coordinate system. + modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) + selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) + + val modalScale = + min( + (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()), + (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()), + ) + val modalPivot = PointF() + getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot) + + newSelectedTaskView.modalScale = modalScale + newSelectedTaskView.modalPivot = modalPivot + } + } + + /** + * Creates a [DesktopTaskView] for the currently active desk on this display, which contains the + * tasks with the given [groupedTaskInfo]. + */ + fun createDesktopTaskViewForActiveDesk(groupedTaskInfo: GroupedTaskInfo): DesktopTaskView { + val desktopTaskView = + recentsView.getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView + val tasks: List = groupedTaskInfo.taskInfoList.map { taskInfo -> Task.from(taskInfo) } + desktopTaskView.bind( + DesktopTask(groupedTaskInfo.deskId, groupedTaskInfo.deskDisplayId, tasks), + recentsView.mOrientationState, + recentsView.mTaskOverlayFactory, + ) + return desktopTaskView + } + + companion object { + class RecentsViewFloatProperty( + private val utilsProperty: KMutableProperty1 + ) : FloatProperty>(utilsProperty.name) { + override fun get(recentsView: RecentsView<*, *>): Float = + utilsProperty.get(recentsView.mUtils) + + override fun setValue(recentsView: RecentsView<*, *>, value: Float) { + utilsProperty.set(recentsView.mUtils, value) + } + } + + @JvmField + val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress) + + val TEMP_RECT = Rect() + } +} diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java index 3d994e89a..4cf1eaecf 100644 --- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java +++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java @@ -16,13 +16,11 @@ package com.android.quickstep.views; -import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON; import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; @@ -41,11 +39,11 @@ import com.android.app.animation.Interpolators; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StateManager; -import com.android.launcher3.states.StateAnimationConfig; +import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.SplitSelectStateController; +import com.android.wm.shell.shared.TypefaceUtils; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; /** * A rounded rectangular component containing a single TextView. @@ -57,7 +55,6 @@ import com.android.quickstep.util.SplitSelectStateController; public class SplitInstructionsView extends LinearLayout { private static final int BOUNCE_DURATION = 250; private static final float BOUNCE_HEIGHT = 20; - private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350; private final RecentsViewContainer mContainer; public boolean mIsCurrentlyAnimating = false; @@ -130,33 +127,32 @@ public class SplitInstructionsView extends LinearLayout { TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel); TextView instructionTextView = findViewById(R.id.split_instructions_text); - if (FeatureFlags.enableSplitContextually()) { - cancelTextView.setVisibility(VISIBLE); - cancelTextView.setOnClickListener((v) -> exitSplitSelection()); - instructionTextView.setText(R.string.toast_contextual_split_select_app); + cancelTextView.setVisibility(VISIBLE); + cancelTextView.setOnClickListener((v) -> exitSplitSelection()); + instructionTextView.setText(R.string.toast_contextual_split_select_app); + TypefaceUtils.setTypeface(instructionTextView, FontFamily.GSF_BODY_MEDIUM); - // After layout, expand touch target of cancel button to meet minimum a11y measurements. - post(() -> { - int minTouchSize = getResources() - .getDimensionPixelSize(settingslib_preferred_minimum_touch_target); - Rect r = new Rect(); - cancelTextView.getHitRect(r); + // After layout, expand touch target of cancel button to meet minimum a11y measurements. + post(() -> { + int minTouchSize = getResources() + .getDimensionPixelSize(settingslib_preferred_minimum_touch_target); + Rect r = new Rect(); + cancelTextView.getHitRect(r); - if (r.width() < minTouchSize) { - // add 1 to ensure ceiling on int division - int expandAmount = (minTouchSize + 1 - r.width()) / 2; - r.left -= expandAmount; - r.right += expandAmount; - } - if (r.height() < minTouchSize) { - int expandAmount = (minTouchSize + 1 - r.height()) / 2; - r.top -= expandAmount; - r.bottom += expandAmount; - } + if (r.width() < minTouchSize) { + // add 1 to ensure ceiling on int division + int expandAmount = (minTouchSize + 1 - r.width()) / 2; + r.left -= expandAmount; + r.right += expandAmount; + } + if (r.height() < minTouchSize) { + int expandAmount = (minTouchSize + 1 - r.height()) / 2; + r.top -= expandAmount; + r.bottom += expandAmount; + } - setTouchDelegate(new TouchDelegate(r, cancelTextView)); - }); - } + setTouchDelegate(new TouchDelegate(r, cancelTextView)); + }); // Set accessibility title, will be announced by a11y tools. instructionTextView.setAccessibilityPaneTitle(instructionTextView.getText()); @@ -165,25 +161,11 @@ public class SplitInstructionsView extends LinearLayout { private void exitSplitSelection() { RecentsView recentsView = mContainer.getOverviewPanel(); SplitSelectStateController splitSelectController = recentsView.getSplitSelectController(); - StateManager stateManager = recentsView.getStateManager(); - BaseState startState = stateManager.getState(); - long duration = startState.getTransitionDuration(mContainer.asContext(), false); - if (duration == 0) { - // Case where we're in contextual on workspace (NORMAL), which by default has 0 - // transition duration - duration = DURATION_DEFAULT_SPLIT_DISMISS; - } - StateAnimationConfig config = new StateAnimationConfig(); - config.duration = duration; - AnimatorSet stateAnim = stateManager.createAtomicAnimation( - startState, NORMAL, config); - AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController() - .createPlaceholderDismissAnim(mContainer, - LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration); - stateAnim.play(dismissAnim); - stateManager.setCurrentAnimation(stateAnim, NORMAL); - stateAnim.start(); + + AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer, + LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, + splitSelectController.getSplitAnimationController()); } void ensureProperRotation() { diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt new file mode 100644 index 000000000..0e769d0d3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt @@ -0,0 +1,235 @@ +/* + * 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.quickstep.views + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.util.Log +import android.view.View +import android.view.View.OnClickListener +import com.android.app.tracing.traceSection +import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.ViewUtils.addAccessibleChildToList +import com.android.quickstep.recents.domain.usecase.ThumbnailPosition +import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData + +/** Holder for all Task dependent information. */ +class TaskContainer( + val taskView: TaskView, + val task: Task, + val snapshotView: View, + val iconView: TaskViewIcon, + /** + * This technically can be a vanilla [android.view.TouchDelegate] class, however that class + * requires setting the touch bounds at construction, so we'd repeatedly be created many + * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch + * delegated bounds only to be updated. + */ + val iconTouchDelegate: TransformingTouchDelegate, + /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ + @SplitConfigurationOptions.StagePosition val stagePosition: Int, + val digitalWellBeingToast: DigitalWellBeingToast?, + val showWindowsView: View?, + taskOverlayFactory: TaskOverlayFactory, +) { + val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this) + var thumbnailPosition: ThumbnailPosition? = null + private var overlayEnabledStatus = false + + init { + if (enableRefactorTaskThumbnail()) { + require(snapshotView is TaskThumbnailView) + } else { + require(snapshotView is TaskThumbnailViewDeprecated) + } + } + + internal var thumbnailData: ThumbnailData? = null + private set + + val thumbnail: Bitmap? + /** If possible don't use this. It should be replaced as part of b/331753115. */ + get() = + if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail + else thumbnailViewDeprecated.thumbnail + + val thumbnailView: TaskThumbnailView + get() { + require(enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailView + } + + val thumbnailViewDeprecated: TaskThumbnailViewDeprecated + get() { + require(!enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailViewDeprecated + } + + var isThumbnailValid: Boolean = false + internal set + + val shouldShowSplashView: Boolean + get() = + if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash() + else thumbnailViewDeprecated.shouldShowSplashView() + + /** Builds proto for logging */ + val itemInfo: TaskViewItemInfo + get() = TaskViewItemInfo(taskView, this) + + fun bind() = + traceSection("TaskContainer.bind") { + digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition) + if (!enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.bind(task, overlay, taskView) + } + } + + fun destroy() = + traceSection("TaskContainer.destroy") { + digitalWellBeingToast?.destroy() + snapshotView.scaleX = 1f + snapshotView.scaleY = 1f + overlay.reset() + if (enableRefactorTaskThumbnail()) { + isThumbnailValid = false + thumbnailData = null + thumbnailView.onRecycle() + } else { + thumbnailViewDeprecated.setShowSplashForSplitSelection(false) + } + + if (enableOverviewIconMenu()) { + (iconView as IconAppChipView).reset() + } + } + + fun setOverlayEnabled(enabled: Boolean) { + if (!enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.setOverlayEnabled(enabled) + } + } + + fun setOverlayEnabled(enabled: Boolean, thumbnailPosition: ThumbnailPosition?) { + if (enableRefactorTaskThumbnail()) { + if (overlayEnabledStatus != enabled || this.thumbnailPosition != thumbnailPosition) { + overlayEnabledStatus = enabled + + refreshOverlay(thumbnailPosition) + } + } + } + + fun refreshOverlay(thumbnailPosition: ThumbnailPosition?) = + traceSection("TaskContainer.refreshOverlay") { + this.thumbnailPosition = thumbnailPosition + when { + !overlayEnabledStatus -> overlay.reset() + thumbnailPosition == null -> { + Log.e(TAG, "Thumbnail position was null during overlay refresh", Exception()) + overlay.reset() + } + else -> + overlay.initOverlay( + task, + thumbnailData?.thumbnail, + thumbnailPosition.matrix, + thumbnailPosition.isRotated, + ) + } + } + + fun addChildForAccessibility(outChildren: ArrayList) { + addAccessibleChildToList(iconView.asView(), outChildren) + addAccessibleChildToList(snapshotView, outChildren) + showWindowsView?.let { addAccessibleChildToList(it, outChildren) } + digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) } + overlay.addChildForAccessibility(outChildren) + } + + fun setState( + state: TaskData?, + liveTile: Boolean, + hasHeader: Boolean, + clickCloseListener: OnClickListener?, + ) = + traceSection("TaskContainer.setState") { + thumbnailView.setState( + TaskUiStateMapper.toTaskThumbnailUiState( + state, + liveTile, + hasHeader, + clickCloseListener, + ), + state?.taskId, + ) + thumbnailData = if (state is TaskData.Data) state.thumbnailData else null + overlay.setThumbnailState(thumbnailData) + } + + fun updateTintAmount(tintAmount: Float) { + thumbnailView.updateTintAmount(tintAmount) + } + + /** + * Updates the progress of the menu opening animation. + * + * This function propagates the given `progress` value to the `thumbnailView` allowing the + * thumbnail view to animate its visual state in sync with the menu's opening/closing + * transition. + * + * @param progress The progress of the menu opening animation (from closed=0 to fully open=1) + */ + fun updateMenuOpenProgress(progress: Float) { + thumbnailView.updateMenuOpenProgress(progress) + } + + /** + * Updates the thumbnail splash progress for a given task. + * + * This function manages the visual feedback of a "splash" effect that can be displayed over a + * thumbnail image, typically during loading or updating. It calculates the alpha (transparency) + * of the splash based on the provided progress and then applies this alpha to the thumbnail + * view if it should be displayed. + * + * @param progress The progress of the operation, ranging from 0.0 to 1.0 + */ + fun updateThumbnailSplashProgress(progress: Float) { + if (enableRefactorTaskThumbnail()) { + thumbnailView.updateSplashAlpha(progress) + } else { + thumbnailViewDeprecated.setSplashAlpha(progress) + } + } + + fun updateThumbnailMatrix(matrix: Matrix) { + thumbnailView.setImageMatrix(matrix) + } + + companion object { + const val TAG = "TaskContainer" + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java deleted file mode 100644 index eda58c590..000000000 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ /dev/null @@ -1,421 +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.quickstep.views; - -import static com.android.app.animation.Interpolators.EMPHASIZED; -import static com.android.launcher3.Flags.enableOverviewIconMenu; -import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Outline; -import android.graphics.Rect; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RectShape; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewOutlineProvider; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.app.animation.Interpolators; -import com.android.launcher3.AbstractFloatingView; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.R; -import com.android.launcher3.anim.AnimationSuccessListener; -import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; -import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.views.BaseDragLayer; -import com.android.quickstep.TaskOverlayFactory; -import com.android.quickstep.TaskUtils; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.TaskCornerRadius; -import com.android.quickstep.views.TaskView.TaskContainer; - -/** - * Contains options for a recent task when long-pressing its icon. - */ -public class TaskMenuView extends AbstractFloatingView { - - private static final Rect sTempRect = new Rect(); - - private static final int REVEAL_OPEN_DURATION = enableOverviewIconMenu() ? 417 : 150; - private static final int REVEAL_CLOSE_DURATION = enableOverviewIconMenu() ? 333 : 100; - - private RecentsViewContainer mContainer; - private TextView mTaskName; - @Nullable - private AnimatorSet mOpenCloseAnimator; - @Nullable - private ValueAnimator mRevealAnimator; - @Nullable private Runnable mOnClosingStartCallback; - private TaskView mTaskView; - private TaskContainer mTaskContainer; - private LinearLayout mOptionLayout; - private float mMenuTranslationYBeforeOpen; - private float mMenuTranslationXBeforeOpen; - - public TaskMenuView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - mContainer = RecentsViewContainer.containerFromContext(context); - setClipToOutline(true); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTaskName = findViewById(R.id.task_name); - mOptionLayout = findViewById(R.id.menu_option_layout); - } - - @Override - public boolean onControllerInterceptTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - BaseDragLayer dl = mContainer.getDragLayer(); - if (!dl.isEventOverView(this, ev)) { - // TODO: log this once we have a new container type for it? - close(true); - return true; - } - } - return false; - } - - @Override - protected void handleClose(boolean animate) { - animateClose(); - } - - @Override - protected boolean isOfType(int type) { - return (type & TYPE_TASK_MENU) != 0; - } - - @Override - public ViewOutlineProvider getOutlineProvider() { - return new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), - TaskCornerRadius.get(view.getContext())); - } - }; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (!(enableOverviewIconMenu() - && ((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView))) { - // TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't break - // additionalTranslationY. - int maxMenuHeight = calculateMaxHeight(); - if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) { - heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST); - } - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - public void onRotationChanged() { - if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { - mOpenCloseAnimator.end(); - } - if (mIsOpen) { - mOptionLayout.removeAllViews(); - if (enableOverviewIconMenu() || !populateAndLayoutMenu()) { - close(false); - } - } - } - - /** - * Show a task menu for the given taskContainer. - */ - public static boolean showForTask(TaskContainer taskContainer, - @Nullable Runnable onClosingStartCallback) { - RecentsViewContainer container = RecentsViewContainer.containerFromContext( - taskContainer.getTaskView().getContext()); - final TaskMenuView taskMenuView = (TaskMenuView) container.getLayoutInflater().inflate( - R.layout.task_menu, container.getDragLayer(), false); - taskMenuView.setOnClosingStartCallback(onClosingStartCallback); - return taskMenuView.populateAndShowForTask(taskContainer); - } - - /** - * Show a task menu for the given taskContainer. - */ - public static boolean showForTask(TaskContainer taskContainer) { - return showForTask(taskContainer, null); - } - - private boolean populateAndShowForTask(TaskContainer taskContainer) { - if (isAttachedToWindow()) { - return false; - } - mContainer.getDragLayer().addView(this); - mTaskView = taskContainer.getTaskView(); - mTaskContainer = taskContainer; - if (!populateAndLayoutMenu()) { - return false; - } - post(this::animateOpen); - return true; - } - - /** @return true if successfully able to populate task view menu, false otherwise */ - private boolean populateAndLayoutMenu() { - addMenuOptions(mTaskContainer); - orientAroundTaskView(mTaskContainer); - return true; - } - - private void addMenuOptions(TaskContainer taskContainer) { - if (enableOverviewIconMenu()) { - removeView(mTaskName); - } else { - mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask())); - mTaskName.setOnClickListener(v -> close(true)); - } - TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer) - .forEach(this::addMenuOption); - } - - private void addMenuOption(SystemShortcut menuOption) { - LinearLayout menuOptionView = (LinearLayout) mContainer.getLayoutInflater().inflate( - R.layout.task_view_menu_option, this, false); - if (enableOverviewIconMenu()) { - ((GradientDrawable) menuOptionView.getBackground()).setCornerRadius(0); - } - menuOption.setIconAndLabelFor( - menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); - LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams(); - mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp, - menuOptionView, mContainer.getDeviceProfile()); - // Set an onClick listener on each menu option. The onClick method is responsible for - // ending LiveTile mode on the thumbnail if needed. - menuOptionView.setOnClickListener(menuOption::onClick); - mOptionLayout.addView(menuOptionView); - } - - private void orientAroundTaskView(TaskContainer taskContainer) { - RecentsView recentsView = mContainer.getOverviewPanel(); - RecentsPagedOrientationHandler orientationHandler = - recentsView.getPagedOrientationHandler(); - measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - - // Get Position - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - mContainer.getDragLayer().getDescendantRectRelativeToSelf( - enableOverviewIconMenu() - ? getIconView().findViewById(R.id.icon_view_menu_anchor) - : taskContainer.getThumbnailViewDeprecated(), - sTempRect); - Rect insets = mContainer.getDragLayer().getInsets(); - BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); - params.width = orientationHandler.getTaskMenuWidth( - taskContainer.getThumbnailViewDeprecated(), deviceProfile, - taskContainer.getStagePosition()); - // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start - params.gravity = Gravity.LEFT; - setLayoutParams(params); - setScaleX(mTaskView.getScaleX()); - setScaleY(mTaskView.getScaleY()); - - // Set divider spacing - ShapeDrawable divider = new ShapeDrawable(new RectShape()); - divider.getPaint().setColor(getResources().getColor(android.R.color.transparent)); - int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing); - mOptionLayout.setShowDividers( - enableOverviewIconMenu() ? SHOW_DIVIDER_NONE : SHOW_DIVIDER_MIDDLE); - - orientationHandler.setTaskOptionsMenuLayoutOrientation( - deviceProfile, mOptionLayout, dividerSpacing, divider); - float thumbnailAlignedX = sTempRect.left - insets.left; - float thumbnailAlignedY = sTempRect.top - insets.top; - - // Changing pivot to make computations easier - // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, - // which would render the X and Y position set here incorrect - setPivotX(0); - setPivotY(0); - setRotation(orientationHandler.getDegreesRotated()); - - if (enableOverviewIconMenu()) { - setTranslationX(thumbnailAlignedX); - setTranslationY(thumbnailAlignedY); - } else { - // Margin that insets the menuView inside the taskView - float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); - setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX, - mTaskContainer.getThumbnailViewDeprecated(), deviceProfile, taskInsetMargin, - getIconView())); - setTranslationY(orientationHandler.getTaskMenuY( - thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(), - mTaskContainer.getStagePosition(), this, taskInsetMargin, - getIconView())); - } - } - - private void animateOpen() { - mMenuTranslationYBeforeOpen = getTranslationY(); - mMenuTranslationXBeforeOpen = getTranslationX(); - animateOpenOrClosed(false); - mIsOpen = true; - } - - private View getIconView() { - return mTaskContainer.getIconView().asView(); - } - - private void animateClose() { - animateOpenOrClosed(true); - } - - private void animateOpenOrClosed(boolean closing) { - if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { - mOpenCloseAnimator.cancel(); - } - mOpenCloseAnimator = new AnimatorSet(); - // If we're opening, we just start from the beginning as a new `TaskMenuView` is created - // each time we do the open animation so there will never be a partial value here. - float revealAnimationStartProgress = 0f; - if (closing && mRevealAnimator != null) { - revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction(); - } - mRevealAnimator = createOpenCloseOutlineProvider() - .createRevealAnimator(this, closing, revealAnimationStartProgress); - mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED - : Interpolators.DECELERATE); - - if (enableOverviewIconMenu()) { - IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView(); - - float additionalTranslationY = 0; - if (((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView)) { - // Animate menu up for enough room to display full menu when task on bottom row. - float menuBottom = getHeight() + mMenuTranslationYBeforeOpen; - float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY(); - float taskbarTop = mContainer.getDeviceProfile().heightPx - - mContainer.getDeviceProfile().getOverviewActionsClaimedSpaceBelow(); - float midpoint = (taskBottom + taskbarTop) / 2f; - additionalTranslationY = -Math.max(menuBottom - midpoint, 0); - } - ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y, - closing ? mMenuTranslationYBeforeOpen - : mMenuTranslationYBeforeOpen + additionalTranslationY); - translationYAnim.setInterpolator(EMPHASIZED); - - ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat( - iconAppChip.getMenuTranslationY(), - MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY); - menuTranslationYAnim.setInterpolator(EMPHASIZED); - - float additionalTranslationX = 0; - if (mContainer.getDeviceProfile().isLandscape - && mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) { - // Animate menu and icon when split task would display off the side of the screen. - additionalTranslationX = Math.max( - getTranslationX() + getWidth() - (mContainer.getDeviceProfile().widthPx - - getResources().getDimensionPixelSize( - R.dimen.task_menu_edge_padding) * 2), 0); - } - - ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, - closing ? mMenuTranslationXBeforeOpen - : mMenuTranslationXBeforeOpen - additionalTranslationX); - translationXAnim.setInterpolator(EMPHASIZED); - - ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat( - iconAppChip.getMenuTranslationX(), - MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX); - menuTranslationXAnim.setInterpolator(EMPHASIZED); - - mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim, - menuTranslationXAnim, menuTranslationYAnim); - } - mOpenCloseAnimator.playTogether(mRevealAnimator, - ObjectAnimator.ofFloat( - mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA, - closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA), - ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); - mOpenCloseAnimator.addListener(new AnimationSuccessListener() { - @Override - public void onAnimationStart(Animator animation) { - setVisibility(VISIBLE); - if (closing && mOnClosingStartCallback != null) { - mOnClosingStartCallback.run(); - } - } - - @Override - public void onAnimationSuccess(Animator animator) { - if (closing) { - closeComplete(); - } - } - }); - mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); - mOpenCloseAnimator.start(); - } - - private void closeComplete() { - mIsOpen = false; - mContainer.getDragLayer().removeView(this); - mRevealAnimator = null; - } - - private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { - float radius = TaskCornerRadius.get(mContext); - Rect fromRect = new Rect( - enableOverviewIconMenu() && isLayoutRtl() ? getWidth() : 0, - 0, - enableOverviewIconMenu() && !isLayoutRtl() ? 0 : getWidth(), - 0); - Rect toRect = new Rect(0, 0, getWidth(), getHeight()); - return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); - } - - /** - * Calculates max height based on how much space we have available. - * If not enough space then the view will scroll. The maximum menu size will sit inside the task - * with a margin on the top and bottom. - */ - private int calculateMaxHeight() { - float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); - return mTaskView.getPagedOrientationHandler().getTaskMenuHeight(taskInsetMargin, - mContainer.getDeviceProfile(), getTranslationX(), getTranslationY()); - } - - private void setOnClosingStartCallback(Runnable onClosingStartCallback) { - mOnClosingStartCallback = onClosingStartCallback; - } -} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt new file mode 100644 index 000000000..f7cb7bef7 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt @@ -0,0 +1,506 @@ +/* + * 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.quickstep.views + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import android.util.AttributeSet +import android.view.Gravity +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.android.app.animation.Interpolators +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.R +import com.android.launcher3.anim.AnimationSuccessListener +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.views.BaseDragLayer +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskUtils +import com.android.quickstep.util.TaskCornerRadius +import java.util.function.Consumer +import kotlin.math.max + +/** Contains options for a recent task when long-pressing its icon. */ +class TaskMenuView +@JvmOverloads +constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int = 0) : + AbstractFloatingView(context, attrs, defStyleAttr) { + private val recentsViewContainer: RecentsViewContainer = + RecentsViewContainer.containerFromContext(context) + private val tempRect = Rect() + private val taskName: TextView by lazy { findViewById(R.id.task_name) } + private val optionLayout: LinearLayout by lazy { findViewById(R.id.menu_option_layout) } + private var openCloseAnimator: AnimatorSet? = null + private var revealAnimator: ValueAnimator? = null + private var onClosingStartCallback: Runnable? = null + private lateinit var taskView: TaskView + private lateinit var taskContainer: TaskContainer + private var menuTranslationXBeforeOpen = 0f + private var menuTranslationYBeforeOpen = 0f + + // Spaced claimed below Overview (taskbar and insets) + private val taskbarTop by lazy { + recentsViewContainer.deviceProfile.heightPx - + recentsViewContainer.deviceProfile.overviewActionsClaimedSpaceBelow + } + private val minMenuTop by lazy { taskContainer.iconView.height.toFloat() } + // TODO(b/401476868): Replace overviewRowSpacing with correct margin to the taskbarTop. + private val maxMenuBottom by lazy { + (taskbarTop - recentsViewContainer.deviceProfile.overviewRowSpacing).toFloat() + } + + init { + clipToOutline = true + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_DOWN) { + if (!recentsViewContainer.dragLayer.isEventOverView(this, ev)) { + // TODO: log this once we have a new container type for it? + animateOpenOrClosed(true) + return true + } + } + return false + } + + override fun handleClose(animate: Boolean) { + animateOpenOrClosed(true, animated = false) + } + + override fun isOfType(type: Int): Boolean = (type and TYPE_TASK_MENU) != 0 + + override fun getOutlineProvider(): ViewOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height, + TaskCornerRadius.get(view.context), + ) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + var heightMeasure = heightMeasureSpec + val maxMenuHeight = calculateMaxHeight() + if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) { + heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST) + } + super.onMeasure(widthMeasureSpec, heightMeasure) + } + + fun onRotationChanged() { + openCloseAnimator?.let { if (it.isRunning) it.end() } + if (mIsOpen) { + optionLayout.removeAllViews() + if (enableOverviewIconMenu() || !populateAndLayoutMenu()) { + close(false) + } + } + } + + private fun populateAndShowForTask(taskContainer: TaskContainer): Boolean { + if (isAttachedToWindow) return false + recentsViewContainer.dragLayer.addView(this) + taskView = taskContainer.taskView + this.taskContainer = taskContainer + if (!populateAndLayoutMenu()) return false + post { this.animateOpen() } + return true + } + + /** @return true if successfully able to populate task view menu, false otherwise */ + private fun populateAndLayoutMenu(): Boolean { + addMenuOptions(taskContainer) + orientAroundTaskView(taskContainer) + return true + } + + private fun addMenuOptions(taskContainer: TaskContainer) { + if (enableOverviewIconMenu()) { + removeView(taskName) + } else { + taskName.text = TaskUtils.getTitle(context, taskContainer.task) + taskName.setOnClickListener { close(true) } + } + TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer) + .forEach(Consumer { menuOption: SystemShortcut<*> -> this.addMenuOption(menuOption) }) + } + + private fun addMenuOption(menuOption: SystemShortcut<*>) { + val menuOptionView = + recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false) + as LinearLayout + if (enableOverviewIconMenu()) { + menuOptionView.background = + ResourcesCompat.getDrawable( + resources, + R.drawable.app_chip_menu_item_bg, + context.theme, + ) + menuOptionView.foreground = + ResourcesCompat.getDrawable( + resources, + R.drawable.app_chip_menu_item_fg, + context.theme, + ) + } + menuOption.setIconAndLabelFor( + menuOptionView.findViewById(R.id.icon), + menuOptionView.findViewById(R.id.text), + ) + val lp = menuOptionView.layoutParams as LayoutParams + taskView.pagedOrientationHandler.setLayoutParamsForTaskMenuOptionItem( + lp, + menuOptionView, + recentsViewContainer.deviceProfile, + ) + // Set an onClick listener on each menu option. The onClick method is responsible for + // ending LiveTile mode on the thumbnail if needed. + menuOptionView.setOnClickListener { v: View? -> menuOption.onClick(v) } + optionLayout.addView(menuOptionView) + } + + private fun orientAroundTaskView(taskContainer: TaskContainer) { + val recentsView = recentsViewContainer.getOverviewPanel>() + val orientationHandler = recentsView.pagedOrientationHandler + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + + // Get Position + val deviceProfile = recentsViewContainer.deviceProfile + recentsViewContainer.dragLayer.getDescendantRectRelativeToSelf( + if (enableOverviewIconMenu()) iconView.findViewById(R.id.icon_view_menu_anchor) + else taskContainer.snapshotView, + tempRect, + ) + val insets = recentsViewContainer.dragLayer.getInsets() + val params = layoutParams as BaseDragLayer.LayoutParams + params.width = + orientationHandler.getTaskMenuWidth( + taskContainer.snapshotView, + deviceProfile, + taskContainer.stagePosition, + ) + // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start + params.gravity = Gravity.LEFT + layoutParams = params + scaleX = taskView.scaleX + scaleY = taskView.scaleY + + // Set divider spacing + val divider = ShapeDrawable(RectShape()) + divider.paint.color = resources.getColor(android.R.color.transparent) + val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt() + optionLayout.showDividers = + if (enableOverviewIconMenu()) SHOW_DIVIDER_NONE else SHOW_DIVIDER_MIDDLE + + optionLayout.background = + if (enableOverviewIconMenu()) { + ResourcesCompat.getDrawable(resources, R.drawable.app_chip_menu_bg, context.theme) + } else { + null + } + + orientationHandler.setTaskOptionsMenuLayoutOrientation( + deviceProfile, + optionLayout, + dividerSpacing, + divider, + ) + val thumbnailAlignedX = (tempRect.left - insets.left).toFloat() + val thumbnailAlignedY = (tempRect.top - insets.top).toFloat() + + // Changing pivot to make computations easier + // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, + // which would render the X and Y position set here incorrect + pivotX = 0f + pivotY = 0f + rotation = orientationHandler.degreesRotated + + if (enableOverviewIconMenu()) { + elevation = resources.getDimension(R.dimen.task_thumbnail_icon_menu_elevation) + translationX = thumbnailAlignedX + translationY = thumbnailAlignedY + } else { + // Margin that insets the menuView inside the taskView + val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin) + translationX = + orientationHandler.getTaskMenuX( + thumbnailAlignedX, + this.taskContainer.snapshotView, + deviceProfile, + taskInsetMargin, + iconView, + ) + translationY = + orientationHandler.getTaskMenuY( + thumbnailAlignedY, + this.taskContainer.snapshotView, + this.taskContainer.stagePosition, + this, + taskInsetMargin, + iconView, + ) + } + } + + private fun animateOpen() { + menuTranslationYBeforeOpen = translationY + menuTranslationXBeforeOpen = translationX + animateOpenOrClosed(false) + mIsOpen = true + } + + private val iconView: View + get() = taskContainer.iconView.asView() + + private fun animateOpenOrClosed(closing: Boolean, animated: Boolean = true) { + openCloseAnimator?.let { if (it.isRunning) it.cancel() } + openCloseAnimator = AnimatorSet() + // If we're opening, we just start from the beginning as a new `TaskMenuView` is created + // each time we do the open animation so there will never be a partial value here. + var revealAnimationStartProgress = 0f + if (closing && revealAnimator != null) { + revealAnimationStartProgress = 1f - revealAnimator!!.animatedFraction + } + revealAnimator = + createOpenCloseOutlineProvider() + .createRevealAnimator(this, closing, revealAnimationStartProgress) + revealAnimator!!.interpolator = + if (enableOverviewIconMenu()) Interpolators.EMPHASIZED else Interpolators.DECELERATE + val openCloseAnimatorBuilder = openCloseAnimator!!.play(revealAnimator) + if (enableOverviewIconMenu()) { + animateOpenOrCloseAppChip(closing, openCloseAnimatorBuilder) + } + openCloseAnimatorBuilder.with( + ObjectAnimator.ofFloat(this, ALPHA, (if (closing) 0 else 1).toFloat()) + ) + if (enableRefactorTaskThumbnail()) { + revealAnimator?.addUpdateListener { animation: ValueAnimator -> + val animatedFraction = animation.animatedFraction + val openProgress = if (closing) (1 - animatedFraction) else animatedFraction + taskContainer.updateMenuOpenProgress(openProgress) + } + } else { + openCloseAnimatorBuilder.with( + ObjectAnimator.ofFloat( + taskContainer.thumbnailViewDeprecated, + TaskThumbnailViewDeprecated.DIM_ALPHA, + if (closing) 0f else TaskView.MAX_PAGE_SCRIM_ALPHA, + ) + ) + } + openCloseAnimator!!.addListener( + object : AnimationSuccessListener() { + override fun onAnimationStart(animation: Animator) { + visibility = VISIBLE + if (closing) onClosingStartCallback?.run() + } + + override fun onAnimationSuccess(animator: Animator) { + if (closing) closeComplete() + } + } + ) + val animationDuration = + when { + animated && closing -> REVEAL_CLOSE_DURATION + animated && !closing -> REVEAL_OPEN_DURATION + else -> 0L + } + openCloseAnimator!!.setDuration(animationDuration) + openCloseAnimator!!.start() + } + + private fun TaskView.isOnGridBottomRow(): Boolean = + (recentsViewContainer.getOverviewPanel() as RecentsView<*, *>).isOnGridBottomRow(this) + + private fun closeComplete() { + mIsOpen = false + recentsViewContainer.dragLayer.removeView(this) + revealAnimator = null + } + + private fun createOpenCloseOutlineProvider(): RoundedRectRevealOutlineProvider { + val radius = TaskCornerRadius.get(mContext) + val fromRect = + Rect( + if (enableOverviewIconMenu() && isLayoutRtl) width else 0, + 0, + if (enableOverviewIconMenu() && !isLayoutRtl) 0 else width, + 0, + ) + val toRect = Rect(0, 0, width, height) + return RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect) + } + + /** + * Calculates max height based on how much space we have available. If not enough space then the + * view will scroll. The maximum menu size will sit inside the task with a margin on the top and + * bottom. + */ + private fun calculateMaxHeight(): Int = + taskView.pagedOrientationHandler.getTaskMenuHeight( + taskInsetMargin = resources.getDimension(R.dimen.task_card_margin), // taskInsetMargin + deviceProfile = recentsViewContainer.deviceProfile, + taskMenuX = translationX, + taskMenuY = + // Bottom menu can translate up to show more options. So we use the min + // translation allowed to calculate its max height. + if (enableOverviewIconMenu() && taskView.isOnGridBottomRow()) minMenuTop + else translationY, + ) + + private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) { + this.onClosingStartCallback = onClosingStartCallback + } + + private fun animateOpenOrCloseAppChip(closing: Boolean, animatorBuilder: AnimatorSet.Builder) { + val iconAppChip = taskContainer.iconView.asView() as IconAppChipView + + // Animate menu up for enough room to display full menu when task on bottom row. + var additionalTranslationY = 0f + if (taskView.isOnGridBottomRow()) { + val currentMenuBottom: Float = menuTranslationYBeforeOpen + height + additionalTranslationY = + if (currentMenuBottom < maxMenuBottom) 0f + // Translate menu up for enough room to display full menu when task on bottom row. + else maxMenuBottom - currentMenuBottom + + val currentMenuTop = menuTranslationYBeforeOpen + additionalTranslationY + // If it translate above the min accepted, it translates to the top of the screen + if (currentMenuTop < minMenuTop) { + // It subtracts the menuTranslation to make it 0 (top of the screen) + chip size. + additionalTranslationY = -menuTranslationYBeforeOpen + minMenuTop + } + } + + val translationYAnim = + ObjectAnimator.ofFloat( + this, + TRANSLATION_Y, + if (closing) menuTranslationYBeforeOpen + else menuTranslationYBeforeOpen + additionalTranslationY, + ) + translationYAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(translationYAnim) + + val menuTranslationYAnim: ObjectAnimator = + ObjectAnimator.ofFloat( + iconAppChip.getMenuTranslationY(), + MultiPropertyFactory.MULTI_PROPERTY_VALUE, + if (closing) 0f else additionalTranslationY, + ) + menuTranslationYAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(menuTranslationYAnim) + + var additionalTranslationX = 0f + if ( + taskContainer.stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + ) { + // Animate menu and icon when split task would display off the side of the screen. + additionalTranslationX = + max( + (translationX + width - + (recentsViewContainer.deviceProfile.widthPx - + resources.getDimensionPixelSize( + R.dimen.task_menu_edge_padding + ) * 2)) + .toDouble(), + 0.0, + ) + .toFloat() + } + + val translationXAnim = + ObjectAnimator.ofFloat( + this, + TRANSLATION_X, + if (closing) menuTranslationXBeforeOpen + else menuTranslationXBeforeOpen - additionalTranslationX, + ) + translationXAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(translationXAnim) + + val menuTranslationXAnim: ObjectAnimator = + ObjectAnimator.ofFloat( + iconAppChip.getMenuTranslationX(), + MultiPropertyFactory.MULTI_PROPERTY_VALUE, + if (closing) 0f else -additionalTranslationX, + ) + menuTranslationXAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(menuTranslationXAnim) + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (enableOverviewIconMenu()) { + if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event) + + val isFirstMenuOptionFocused = optionLayout.indexOfChild(optionLayout.focusedChild) == 0 + val isLastMenuOptionFocused = + optionLayout.indexOfChild(optionLayout.focusedChild) == optionLayout.childCount - 1 + if ( + (isLastMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) || + (isFirstMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_UP) + ) { + iconView.requestFocus() + return true + } + } + return super.dispatchKeyEvent(event) + } + + companion object { + private val REVEAL_OPEN_DURATION = if (enableOverviewIconMenu()) 417L else 150L + private val REVEAL_CLOSE_DURATION = if (enableOverviewIconMenu()) 333L else 100L + + /** Show a task menu for the given taskContainer. */ + /** Show a task menu for the given taskContainer. */ + @JvmOverloads + fun showForTask( + taskContainer: TaskContainer, + onClosingStartCallback: Runnable? = null, + ): Boolean { + val container: RecentsViewContainer = + RecentsViewContainer.containerFromContext(taskContainer.taskView.context) + val taskMenuView = + container.layoutInflater.inflate(R.layout.task_menu, container.dragLayer, false) + as TaskMenuView + taskMenuView.setOnClosingStartCallback(onClosingStartCallback) + return taskMenuView.populateAndShowForTask(taskContainer) + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt index 659cc0c25..19d706fbb 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt +++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt @@ -37,13 +37,16 @@ import com.android.launcher3.popup.RoundedArrowDrawable import com.android.launcher3.popup.SystemShortcut import com.android.launcher3.util.Themes import com.android.quickstep.TaskOverlayFactory -import com.android.quickstep.views.TaskView.TaskContainer class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T : Context { companion object { const val TAG = "TaskMenuViewWithArrow" - fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean { + fun showForTask( + taskContainer: TaskContainer, + alignedOptionIndex: Int = 0, + onClosedCallback: Runnable? = null + ): Boolean { val container: RecentsViewContainer = RecentsViewContainer.containerFromContext(taskContainer.taskView.context) val taskMenuViewWithArrow = @@ -53,12 +56,18 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T false ) as TaskMenuViewWithArrow<*> - return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex) + return taskMenuViewWithArrow.populateAndShowForTask( + taskContainer, + alignedOptionIndex, + onClosedCallback + ) } } constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor( context: Context, attrs: AttributeSet, @@ -80,6 +89,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private var alignedOptionIndex: Int = 0 private val extraSpaceForRowAlignment: Int get() = optionMeasuredHeight * alignedOptionIndex + private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin) private lateinit var taskView: TaskView @@ -89,13 +99,14 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private var optionMeasuredHeight = 0 private val arrowHorizontalPadding: Int get() = - if (taskView.isFocusedTask) + if (taskView.isLargeTile) resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding) else 0 private var iconView: IconView? = null private var scrim: View? = null private val scrimAlpha = 0.8f + private var onClosedCallback: Runnable? = null override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0 @@ -139,7 +150,8 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private fun populateAndShowForTask( taskContainer: TaskContainer, - alignedOptionIndex: Int + alignedOptionIndex: Int, + onClosedCallback: Runnable? ): Boolean { if (isAttachedToWindow) { return false @@ -148,6 +160,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T taskView = taskContainer.taskView this.taskContainer = taskContainer this.alignedOptionIndex = alignedOptionIndex + this.onClosedCallback = onClosedCallback if (!populateMenu()) return false addScrim() show() @@ -250,6 +263,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T super.closeComplete() popupContainer.removeView(scrim) popupContainer.removeView(iconView) + onClosedCallback?.run() } /** diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java index 4283d0e4c..74d76e643 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java @@ -28,16 +28,13 @@ import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; -import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.os.Build; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Property; @@ -45,18 +42,16 @@ import android.view.View; import android.widget.ImageView; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; -import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags; import com.android.launcher3.util.ViewPool; +import com.android.quickstep.FullscreenDrawParams; import com.android.quickstep.TaskOverlayFactory.TaskOverlay; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.views.TaskView.FullscreenDrawParams; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; @@ -70,8 +65,6 @@ import java.util.Objects; */ @Deprecated public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusable { - private static final MainThreadInitializedObject TEMP_PARAMS = - new MainThreadInitializedObject<>(FullscreenDrawParams::new); public static final Property DIM_ALPHA = new FloatProperty("dimAlpha") { @@ -99,36 +92,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } }; - /** Use to animate thumbnail translationX while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_X = - new FloatProperty("splitSelectTranslateX") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateX) { - thumbnail.applySplitSelectTranslateX(splitSelectTranslateX); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateX; - } - }; - - /** Use to animate thumbnail translationY while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_Y = - new FloatProperty("splitSelectTranslateY") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateY) { - thumbnail.applySplitSelectTranslateY(splitSelectTranslateY); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateY; - } - }; - private final RecentsViewContainer mContainer; private TaskOverlay mOverlay; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -141,9 +104,10 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. private final Rect mPreviewRect = new Rect(); private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); - private TaskView.FullscreenDrawParams mFullscreenParams; + private FullscreenDrawParams mFullscreenParams; private ImageView mSplashView; private Drawable mSplashViewDrawable; + private TaskView mTaskView; @Nullable private Task mTask; @@ -160,8 +124,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab private boolean mOverlayEnabled; /** Used as a placeholder when the original thumbnail animates out to. */ private boolean mShowSplashForSplitSelection; - private float mSplitSelectTranslateX; - private float mSplitSelectTranslateY; public TaskThumbnailViewDeprecated(Context context) { this(context, null); @@ -180,8 +142,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mContainer = RecentsViewContainer.containerFromContext(context); // Initialize with placeholder value. It is overridden later by TaskView - mFullscreenParams = TEMP_PARAMS.get(context); - + mFullscreenParams = new FullscreenDrawParams(context, __ -> 0f, __ -> 0f); mDimColor = RecentsView.getForegroundScrimDimColor(context); mDimmingPaintAfterClearing.setColor(mDimColor); } @@ -189,10 +150,11 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab /** * Updates the thumbnail to draw the provided task */ - public void bind(Task task, TaskOverlay overlay) { + public void bind(Task task, TaskOverlay overlay, TaskView taskView) { mOverlay = overlay; mOverlay.reset(); mTask = task; + mTaskView = taskView; int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; mPaint.setColor(color); mBackgroundPaint.setColor(color); @@ -200,17 +162,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab updateSplashView(mTask.icon); } - /** - * Sets TaskOverlay without binding a task. - * - * @deprecated Should only be used when using new - * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}. - */ - @Deprecated - public void setTaskOverlay(TaskOverlay overlay) { - mOverlay = overlay; - } - /** * Updates the thumbnail. * @@ -298,40 +249,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mDimAlpha; } - /** - * Get the scaled insets that are being used to draw the task view. This is a subsection of - * the full snapshot. - * - * @return the insets in snapshot bitmap coordinates. - */ - @RequiresApi(api = Build.VERSION_CODES.Q) - public Insets getScaledInsets() { - if (mThumbnailData == null) { - return Insets.NONE; - } - - RectF bitmapRect = new RectF( - 0, - 0, - mThumbnailData.getThumbnail().getWidth(), - mThumbnailData.getThumbnail().getHeight()); - RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); - - // The position helper matrix tells us how to transform the bitmap to fit the view, the - // inverse tells us where the view would be in the bitmaps coordinates. The insets are the - // difference between the bitmap bounds and the projected view bounds. - Matrix boundsToBitmapSpace = new Matrix(); - mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace); - RectF boundsInBitmapSpace = new RectF(); - boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); - - DeviceProfile dp = mContainer.getDeviceProfile(); - int bottomInset = dp.isTablet - ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0; - return Insets.of(0, 0, 0, bottomInset); - } - - @SystemUiControllerFlags public int getSysUiStatusNavFlags() { if (mThumbnailData != null) { @@ -358,7 +275,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab canvas.save(); // Draw the insets if we're being drawn fullscreen (we do this for quick switch). drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), - mFullscreenParams.getCurrentDrawnCornerRadius()); + mFullscreenParams.getCurrentCornerRadius()); canvas.restore(); } @@ -366,15 +283,15 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mPreviewPositionHelper; } - public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { + public void setFullscreenParams(FullscreenDrawParams fullscreenParams) { mFullscreenParams = fullscreenParams; invalidate(); } public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius) { - if (mTask != null && getTaskView().isRunningTask() - && !getTaskView().getShouldShowScreenshot()) { + if (mTask != null && mTaskView.isRunningTask() + && !mTaskView.getShouldShowScreenshot()) { canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mDimmingPaintAfterClearing); @@ -415,35 +332,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } } - /** See {@link #SPLIT_SELECT_TRANSLATE_X} */ - protected void applySplitSelectTranslateX(float splitSelectTranslateX) { - mSplitSelectTranslateX = splitSelectTranslateX; - applyTranslateX(); - } - - /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */ - protected void applySplitSelectTranslateY(float splitSelectTranslateY) { - mSplitSelectTranslateY = splitSelectTranslateY; - applyTranslateY(); - } - - private void applyTranslateX() { - setTranslationX(mSplitSelectTranslateX); - } - - private void applyTranslateY() { - setTranslationY(mSplitSelectTranslateY); - } - - protected void resetViewTransforms() { - mSplitSelectTranslateX = 0; - mSplitSelectTranslateY = 0; - } - - public TaskView getTaskView() { - return (TaskView) getParent(); - } - public void setOverlayEnabled(boolean overlayEnabled) { if (mOverlayEnabled != overlayEnabled) { mOverlayEnabled = overlayEnabled; @@ -496,9 +384,9 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab float viewCenterY = viewHeight / 2f; float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f; float centeredDrawableTop = (viewHeight - drawableHeight) / 2f; - float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale(); - float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null - ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen(); + float nonGridScale = mTaskView == null ? 1 : 1 / mTaskView.getNonGridScale(); + float recentsMaxScale = mTaskView == null || mTaskView.getRecentsView() == null + ? 1 : 1 / mTaskView.getRecentsView().getMaxScaleForFullScreen(); float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX()); float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY()); @@ -524,8 +412,13 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); } - private boolean isThumbnailRotationDifferentFromTask() { - RecentsView recents = getTaskView().getRecentsView(); + /** + * Returns whether or not the current thumbnail is a different orientation to the task. + *

+ * Used to disable modal state when screenshot doesn't match the device orientation. + */ + public boolean isThumbnailRotationDifferentFromTask() { + RecentsView recents = mTaskView.getRecentsView(); if (recents == null || mThumbnailData == null) { return false; } @@ -544,7 +437,9 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab */ private void refreshOverlay() { if (mOverlayEnabled) { - mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(), + mOverlay.initOverlay(mTask, + mThumbnailData != null ? mThumbnailData.getThumbnail() : null, + mPreviewPositionHelper.getMatrix(), mPreviewPositionHelper.isOrientationChanged()); } else { mOverlay.reset(); @@ -571,7 +466,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab if (mBitmapShader != null && mThumbnailData != null) { mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(), mThumbnailData.getThumbnail().getHeight()); - int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation(); + int currentRotation = mTaskView.getOrientedState().getRecentsActivityRotation(); boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl); @@ -579,7 +474,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix()); mPaint.setShader(mBitmapShader); } - getTaskView().updateCurrentFullscreenParams(); + mTaskView.updateFullscreenParams(); invalidate(); } @@ -617,6 +512,10 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mThumbnailData.isRealSnapshot && !mTask.isLocked; } + public Matrix getThumbnailMatrix() { + return mPreviewPositionHelper.getMatrix(); + } + @Override public void onRecycle() { // Do nothing diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt new file mode 100644 index 000000000..9a8805bf0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import com.android.launcher3.R +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader + +class TaskThumbnailViewHeader +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + + private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) } + private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) } + private val headerCloseButton: ImageButton by lazy { findViewById(R.id.header_close_button) } + + fun setHeader(header: ThumbnailHeader) { + headerTitleView.setText(header.title) + headerIconView.setImageDrawable(header.icon) + headerCloseButton.setOnClickListener(header.clickCloseListener) + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 94ebc5a2d..fa3fd9161 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -22,7 +22,6 @@ import android.animation.ObjectAnimator import android.annotation.IdRes import android.app.ActivityOptions import android.content.Context -import android.content.Intent import android.graphics.Canvas import android.graphics.PointF import android.graphics.Rect @@ -38,58 +37,58 @@ import android.view.View.OnClickListener import android.view.ViewGroup import android.view.ViewStub import android.view.accessibility.AccessibilityNodeInfo -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.FrameLayout import android.widget.Toast import androidx.annotation.IntDef import androidx.annotation.VisibleForTesting import androidx.core.view.updateLayoutParams import com.android.app.animation.Interpolators +import com.android.app.tracing.traceSection +import com.android.launcher3.AbstractFloatingView import com.android.launcher3.Flags.enableCursorHoverStates -import com.android.launcher3.Flags.enableFocusOutline +import com.android.launcher3.Flags.enableDesktopExplodedView import com.android.launcher3.Flags.enableGridOnlyOverview +import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview +import com.android.launcher3.Flags.enableLargeDesktopWindowingTile import com.android.launcher3.Flags.enableOverviewIconMenu import com.android.launcher3.Flags.enableRefactorTaskThumbnail -import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag -import com.android.launcher3.LauncherSettings +import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.anim.AnimatedFloat -import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH import com.android.launcher3.logging.StatsLogManager.LauncherEvent import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.ItemInfoWithIcon -import com.android.launcher3.model.data.WorkspaceItemInfo -import com.android.launcher3.pm.UserCache +import com.android.launcher3.model.data.TaskViewItemInfo import com.android.launcher3.testing.TestLogging import com.android.launcher3.testing.shared.TestProtocol import com.android.launcher3.util.CancellableTask -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Executors +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate import com.android.launcher3.util.MultiPropertyFactory -import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE +import com.android.launcher3.util.MultiValueAlpha import com.android.launcher3.util.RunnableList -import com.android.launcher3.util.SafeCloseable -import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.util.SplitConfigurationOptions.StagePosition import com.android.launcher3.util.TraceHelper import com.android.launcher3.util.TransformingTouchDelegate import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.coroutines.DispatcherProvider import com.android.launcher3.util.rects.set -import com.android.launcher3.views.ActivityContext +import com.android.quickstep.FullscreenDrawParams import com.android.quickstep.RecentsModel import com.android.quickstep.RemoteAnimationTargets -import com.android.quickstep.TaskAnimationManager +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle import com.android.quickstep.TaskOverlayFactory -import com.android.quickstep.TaskOverlayFactory.TaskOverlay -import com.android.quickstep.TaskUtils import com.android.quickstep.TaskViewUtils import com.android.quickstep.orientation.RecentsPagedOrientationHandler -import com.android.quickstep.task.thumbnail.TaskThumbnail -import com.android.quickstep.task.thumbnail.TaskThumbnailView -import com.android.quickstep.task.viewmodel.TaskViewData +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.recents.di.inject +import com.android.quickstep.recents.domain.usecase.ThumbnailPosition +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState +import com.android.quickstep.recents.ui.viewmodel.TaskViewModel import com.android.quickstep.util.ActiveGestureErrorDetector import com.android.quickstep.util.ActiveGestureLog import com.android.quickstep.util.BorderAnimator @@ -97,11 +96,20 @@ import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAni import com.android.quickstep.util.RecentsOrientedState import com.android.quickstep.util.TaskCornerRadius import com.android.quickstep.util.TaskRemovedDuringLaunchListener +import com.android.quickstep.util.isExternalDisplay +import com.android.quickstep.util.safeDisplayId +import com.android.quickstep.views.IconAppChipView.AppChipStatus +import com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL +import com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.system.ActivityManagerWrapper -import com.android.systemui.shared.system.QuickStepContract +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** A task in the Recents view. */ open class TaskView @@ -112,7 +120,9 @@ constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0, focusBorderAnimator: BorderAnimator? = null, - hoverBorderAnimator: BorderAnimator? = null + hoverBorderAnimator: BorderAnimator? = null, + val type: TaskViewType = TaskViewType.SINGLE, + protected val thumbnailFullscreenParams: FullscreenDrawParams = FullscreenDrawParams(context), ) : FrameLayout(context, attrs), ViewPool.Reusable { /** * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which @@ -122,37 +132,38 @@ constructor( @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS) annotation class TaskDataChanges - /** Type of task view */ - @Retention(AnnotationRetention.SOURCE) - @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP) - annotation class Type { - companion object { - const val SINGLE = 1 - const val GROUPED = 2 - const val DESKTOP = 3 - } - } - - val taskViewData = TaskViewData() val taskIds: IntArray /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ get() = taskContainers.map { it.task.key.id }.toIntArray() - val thumbnailViews: Array - get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray() + val taskIdSet: Set + /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ + get() = taskContainers.map { it.task.key.id }.toSet() + + val snapshotViews: Array + get() = taskContainers.map { it.snapshotView }.toTypedArray() val isGridTask: Boolean /** Returns whether the task is part of overview grid and not being focused. */ - get() = container.deviceProfile.isTablet && !isFocusedTask + get() = container.deviceProfile.isTablet && !isLargeTile val isRunningTask: Boolean get() = this === recentsView?.runningTaskView - val isFocusedTask: Boolean - get() = this === recentsView?.focusedTaskView + private val isSelectedTask: Boolean + get() = this === recentsView?.selectedTaskView - val taskCornerRadius: Float - get() = currentFullscreenParams.cornerRadius + open val displayId: Int + get() = taskContainers.firstOrNull()?.task.safeDisplayId + + val isExternalDisplay: Boolean + get() = displayId.isExternalDisplay + + val isLargeTile: Boolean + get() = + this == recentsView?.focusedTaskView || + (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP) || + (enableSeparateExternalDisplayTasks() && isExternalDisplay) val recentsView: RecentsView<*, *>? get() = parent as? RecentsView<*, *> @@ -160,21 +171,25 @@ constructor( val pagedOrientationHandler: RecentsPagedOrientationHandler get() = orientedState.orientationHandler - @get:Deprecated("Use [taskContainers] instead.") - val firstTask: Task + val firstTaskContainer: TaskContainer? + get() = taskContainers.firstOrNull() + + val firstTask: Task? /** Returns the first task bound to this TaskView. */ - get() = taskContainers[0].task + get() = firstTaskContainer?.task - @get:Deprecated("Use [taskContainers] instead.") - val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated - /** Returns the first thumbnailView of the TaskView. */ - get() = taskContainers[0].thumbnailViewDeprecated + val firstItemInfo: ItemInfo? + get() = firstTaskContainer?.itemInfo - @get:Deprecated("Use [taskContainers] instead.") - val firstItemInfo: ItemInfo - get() = taskContainers[0].itemInfo + /** + * A [TaskViewItemInfo] of this TaskView. The [firstTaskContainer] will be used to get some + * specific information like user, title etc of the Task. However, these task specific + * information will be skipped if the TaskView has no [taskContainers]. Note, please use + * [TaskContainer.itemInfo] for [TaskViewItemInfo] on a specific [TaskContainer]. + */ + val itemInfo: TaskViewItemInfo + get() = TaskViewItemInfo(this, firstTaskContainer) - private val currentFullscreenParams = FullscreenDrawParams(context) protected val container: RecentsViewContainer = RecentsViewContainer.containerFromContext(context) protected val lastTouchDownPosition = PointF() @@ -192,12 +207,9 @@ constructor( * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does * not change according to a temporary state (e.g. task offset). */ - get() = - (getNonGridTrans(nonGridTranslationX) + - getGridTrans(this.gridTranslationX) + - getNonGridTrans(nonGridPivotTranslationX)) + get() = (getNonGridTrans(nonGridTranslationX) + getGridTrans(this.gridTranslationX)) - protected val persistentTranslationY: Float + val persistentTranslationY: Float /** * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does * not change according to a temporary state (e.g. task offset). @@ -208,21 +220,21 @@ constructor( get() = pagedOrientationHandler.getPrimaryValue( SPLIT_SELECT_TRANSLATION_X, - SPLIT_SELECT_TRANSLATION_Y + SPLIT_SELECT_TRANSLATION_Y, ) protected val secondarySplitTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( SPLIT_SELECT_TRANSLATION_X, - SPLIT_SELECT_TRANSLATION_Y + SPLIT_SELECT_TRANSLATION_Y, ) - protected val primaryDismissTranslationProperty: FloatProperty + val primaryDismissTranslationProperty: FloatProperty get() = pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) - protected val secondaryDismissTranslationProperty: FloatProperty + val secondaryDismissTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) @@ -230,26 +242,61 @@ constructor( get() = pagedOrientationHandler.getPrimaryValue( TASK_OFFSET_TRANSLATION_X, - TASK_OFFSET_TRANSLATION_Y + TASK_OFFSET_TRANSLATION_Y, ) protected val secondaryTaskOffsetTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( TASK_OFFSET_TRANSLATION_X, - TASK_OFFSET_TRANSLATION_Y + TASK_OFFSET_TRANSLATION_Y, ) protected val taskResistanceTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( TASK_RESISTANCE_TRANSLATION_X, - TASK_RESISTANCE_TRANSLATION_Y + TASK_RESISTANCE_TRANSLATION_Y, ) private val tempCoordinates = FloatArray(2) - private val focusBorderAnimator: BorderAnimator? - private val hoverBorderAnimator: BorderAnimator? + private val focusBorderAnimator: BorderAnimator? = + focusBorderAnimator + ?: createSimpleBorderAnimator( + TaskCornerRadius.get(context).toInt(), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getThumbnailBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes) + .getColor( + R.styleable.TaskView_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + + private val hoverBorderAnimator: BorderAnimator? = + hoverBorderAnimator + ?: if (enableCursorHoverStates()) + createSimpleBorderAnimator( + TaskCornerRadius.get(context).toInt(), + context.resources.getDimensionPixelSize(R.dimen.task_hover_border_width), + this::getThumbnailBounds, + this, + context + .obtainStyledAttributes( + attrs, + R.styleable.TaskView, + defStyleAttr, + defStyleRes, + ) + .getColor( + R.styleable.TaskView_hoverBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + else null + private val rootViewDisplayId: Int get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY @@ -261,11 +308,17 @@ constructor( var taskViewId = UNBOUND_TASK_VIEW_ID var isEndQuickSwitchCuj = false + var sysUiStatusNavFlags: Int = 0 + get() = + if (enableRefactorTaskThumbnail()) field + else firstTaskContainer?.thumbnailViewDeprecated?.sysUiStatusNavFlags ?: 0 + private set // Various animation progress variables. // progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. protected var fullscreenProgress = 0f set(value) { + if (value == field && enableOverviewIconMenu()) return field = Utilities.boundToRange(value, 0f, 1f) onFullscreenProgressChanged(field) } @@ -290,6 +343,18 @@ constructor( onModalnessUpdated(field) } + var modalPivot: PointF? = null + set(value) { + field = value + updatePivots() + } + + var splitSplashAlpha = 0f + set(value) { + field = value + applyThumbnailSplashAlpha() + } + protected var taskThumbnailSplashAlpha = 0f set(value) { field = value @@ -308,6 +373,12 @@ constructor( applyScale() } + var modalScale = 1f + set(value) { + field = value + applyScale() + } + private var dismissTranslationX = 0f set(value) { field = value @@ -379,12 +450,6 @@ constructor( applyTranslationX() } - protected var nonGridPivotTranslationX = 0f - set(value) { - field = value - applyTranslationX() - } - // Used when in SplitScreenSelectState private var splitSelectTranslationY = 0f set(value) { @@ -398,14 +463,15 @@ constructor( applyTranslationX() } - protected var stableAlpha = 1f - set(value) { - field = value - alpha = stableAlpha - } + private val taskViewAlpha = MultiValueAlpha(this, Alpha.entries.size) + protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Stable) + var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Attach) + var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Split) + private var modalAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Modal) protected var shouldShowScreenshot = false get() = !isRunningTask || field + private set /** Enable or disable showing border on hover and focus change */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @@ -421,37 +487,79 @@ constructor( focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) } - private var focusTransitionProgress = 1f + /** + * Used to cache the hover border state so we don't repeatedly call the border animator with + * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds]. + */ + private var hoverBorderVisible = false set(value) { + if (field == value) { + return + } field = value - onFocusTransitionProgressUpdated(field) + Log.d( + TAG, + "${taskIds.contentToString()} - setting border animator visibility to: $field", + ) + hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true) } - private val focusTransitionPropertyFactory = + // Used to cache thumbnail bounds to avoid recalculating on every hover move. + private var thumbnailBounds = Rect() + + // Progress variable indicating if the TaskView is in a settled state: + // 0 = The TaskView is in a transitioning state e.g. during gesture, in quickswitch carousel, + // becoming focus task etc. + // 1 = The TaskView is settled and no longer transitioning + private var settledProgress = 1f + set(value) { + if (value == field && enableOverviewIconMenu()) return + field = value + onSettledProgressUpdated(field) + } + + private val settledProgressPropertyFactory = MultiPropertyFactory( this, - FOCUS_TRANSITION, - FOCUS_TRANSITION_INDEX_COUNT, + SETTLED_PROGRESS, + SettledProgress.entries.size, { x: Float, y: Float -> x * y }, - 1f + 1f, ) - private val focusTransitionFullscreen = - focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN) - private val focusTransitionScaleAndDim = - focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM) + private var settledProgressFullscreen by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Fullscreen) + private var settledProgressGesture by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Gesture) + private var settledProgressDismiss by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Dismiss) + + private var viewModel: TaskViewModel? = null + private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject() + private val coroutineScope: CoroutineScope by RecentsDependencies.inject() + private val coroutineJobs = mutableListOf() /** - * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in + * Returns an animator of [settledProgressDismiss] that transition in with a built-in * interpolator. */ - fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator = + fun getDismissIconFadeInAnimator(): ObjectAnimator = + ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_DISMISS, 1f).apply { + duration = FADE_IN_ICON_DURATION + interpolator = FADE_IN_ICON_INTERPOLATOR + } + + /** + * Returns an animator of [settledProgressDismiss] that transition out with a built-in + * interpolator. [AnimatedFloat] is used to apply another level of interpolation, on top of + * interpolator set to the [Animator] by the caller. + */ + fun getDismissIconFadeOutAnimator(): ObjectAnimator = AnimatedFloat { v -> - focusTransitionScaleAndDim.value = - FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v) + settledProgressDismiss = SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v) } .animateToValue(1f, 0f) - private var iconAndDimAnimator: ObjectAnimator? = null + private var iconFadeInOnGestureCompleteAnimator: ObjectAnimator? = null // The current background requests to load the task thumbnail and icon private val pendingThumbnailLoadRequests = mutableListOf>() private val pendingIconLoadRequests = mutableListOf>() @@ -459,51 +567,15 @@ constructor( init { setOnClickListener { _ -> onClick() } - val keyboardFocusHighlightEnabled = - (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline()) - val cursorHoverStatesEnabled = enableCursorHoverStates() - setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled) - context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use { - this.focusBorderAnimator = - focusBorderAnimator - ?: if (keyboardFocusHighlightEnabled) - createSimpleBorderAnimator( - currentFullscreenParams.cornerRadius.toInt(), - context.resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_border_width - ), - { bounds: Rect -> getThumbnailBounds(bounds) }, - this, - it.getColor( - R.styleable.TaskView_focusBorderColor, - BorderAnimator.DEFAULT_BORDER_COLOR - ) - ) - else null - this.hoverBorderAnimator = - hoverBorderAnimator - ?: if (cursorHoverStatesEnabled) - createSimpleBorderAnimator( - currentFullscreenParams.cornerRadius.toInt(), - context.resources.getDimensionPixelSize( - R.dimen.task_hover_border_width - ), - { bounds: Rect -> getThumbnailBounds(bounds) }, - this, - it.getColor( - R.styleable.TaskView_hoverBorderColor, - BorderAnimator.DEFAULT_BORDER_COLOR - ) - ) - else null - } + + setWillNotDraw(!enableCursorHoverStates()) } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public override fun onFocusChanged( gainFocus: Boolean, direction: Int, - previouslyFocusedRect: Rect? + previouslyFocusedRect: Rect?, ) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) if (borderEnabled) { @@ -514,20 +586,28 @@ constructor( override fun onHoverEvent(event: MotionEvent): Boolean { if (borderEnabled) { when (event.action) { - MotionEvent.ACTION_HOVER_ENTER -> - hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true) - MotionEvent.ACTION_HOVER_EXIT -> - hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true) + MotionEvent.ACTION_HOVER_ENTER -> { + hoverBorderVisible = + if (enableHoverOfChildElementsInTaskview()) { + getThumbnailBounds(thumbnailBounds) + event.isWithinThumbnailBounds() + } else { + true + } + } + MotionEvent.ACTION_HOVER_MOVE -> + if (enableHoverOfChildElementsInTaskview()) + hoverBorderVisible = event.isWithinThumbnailBounds() + MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false else -> {} } } return super.onHoverEvent(event) } - // avoid triggering hover event on child elements which would cause HOVER_EXIT for this - // task view - override fun onInterceptHoverEvent(event: MotionEvent) = - if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event) + override fun onInterceptHoverEvent(event: MotionEvent): Boolean = + if (enableHoverOfChildElementsInTaskview()) super.onInterceptHoverEvent(event) + else if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event) override fun dispatchTouchEvent(ev: MotionEvent): Boolean { val recentsView = recentsView ?: return false @@ -555,36 +635,69 @@ constructor( super.draw(canvas) } + override fun setLayoutDirection(layoutDirection: Int) { + super.setLayoutDirection(layoutDirection) + if (enableOverviewIconMenu()) { + val deviceLayoutDirection = resources.configuration.layoutDirection + taskContainers.forEach { + (it.iconView as IconAppChipView).layoutDirection = deviceLayoutDirection + } + } + } + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) - val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx - if (container.deviceProfile.isTablet) { - pivotX = (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() - pivotY = thumbnailTopMargin.toFloat() - } else { - pivotX = (right - left) * 0.5f - pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f - } + updatePivots() systemGestureExclusionRects = SYSTEM_GESTURE_EXCLUSION_RECT.onEach { it.right = width it.bottom = height } + if (enableHoverOfChildElementsInTaskview()) { + getThumbnailBounds(thumbnailBounds) + } + } + + private fun updatePivots() { + val modalPivot = modalPivot + if (modalPivot != null) { + pivotX = modalPivot.x + pivotY = modalPivot.y + } else { + val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx + if (container.deviceProfile.isTablet) { + pivotX = + (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() + pivotY = thumbnailTopMargin.toFloat() + } else { + pivotX = (right - left) * 0.5f + pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f + } + } } override fun onRecycle() { resetPersistentViewTransforms() + + viewModel = null + attachAlpha = 1f + splitAlpha = 1f + splitSplashAlpha = 0f + modalAlpha = 1f + modalScale = 1f + modalPivot = null + taskThumbnailSplashAlpha = 0f // Clear any references to the thumbnail (it will be re-read either from the cache or the // system on next bind) - if (enableRefactorTaskThumbnail()) { - notifyIsRunningTaskUpdated() - } else { + if (!enableRefactorTaskThumbnail()) { taskContainers.forEach { it.thumbnailViewDeprecated.setThumbnail(it.task, null) } } setOverlayEnabled(false) onTaskListVisibilityChanged(false) borderEnabled = false + hoverBorderVisible = false taskViewId = UNBOUND_TASK_VIEW_ID + // TODO(b/390583187): Clean the components UI State when TaskView is recycled. taskContainers.forEach { it.destroy() } } @@ -594,34 +707,36 @@ constructor( override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(info) with(info) { - addAction( - AccessibilityAction( - R.id.action_close, - context.getText(R.string.accessibility_close) - ) - ) - - taskContainers.forEach { - TraceHelper.allowIpcs("TV.a11yInfo") { - TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut -> - addAction(shortcut.createAccessibilityAction(context)) + // Only make actions available if the app icon menu is visible to the user. + // When modalness is >0, the user is in select mode and the icon menu is hidden. + // When split selection is active, they should only be able to select the app and not + // take any other action. + val shouldPopulateAccessibilityMenu = + modalness == 0f && recentsView?.isSplitSelectionActive == false + if (shouldPopulateAccessibilityMenu) { + taskContainers.forEach { + TraceHelper.allowIpcs("TV.a11yInfo") { + TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut + -> + addAction(shortcut.createAccessibilityAction(context)) + } } } - } - // Add DWB accessibility action at the end of the list - taskContainers.forEach { - it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction) + // Add DWB accessibility action at the end of the list + taskContainers.forEach { + it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction) + } } recentsView?.let { collectionItemInfo = - AccessibilityNodeInfo.CollectionItemInfo.obtain( + AccessibilityNodeInfo.CollectionItemInfo( 0, 1, - it.taskViewCount - it.indexOfChild(this@TaskView) - 1, + it.getAccessibilityChildren().indexOf(this@TaskView), 1, - false + false, ) } } @@ -629,11 +744,6 @@ constructor( override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean { // TODO(b/343708271): Add support for multiple tasks per action. - if (action == R.id.action_close) { - recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/) - return true - } - taskContainers.forEach { if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) { return true @@ -650,13 +760,161 @@ constructor( return super.performAccessibilityAction(action, arguments) } + override fun onFinishInflate() { + super.onFinishInflate() + inflateViewStubs() + } + + protected open fun inflateViewStubs() { + findViewById(R.id.snapshot) + ?.apply { + layoutResource = + if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail + else R.layout.task_thumbnail_deprecated + } + ?.inflate() + findViewById(R.id.icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + } + + override fun onAttachedToWindow() = + traceSection("TaskView.onAttachedToWindow") { + super.onAttachedToWindow() + if (enableRefactorTaskThumbnail()) { + // The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in + // onRecycle. So it should be initialized at this point. TaskView Lifecycle: + // `bind` -> `onBind` -> onAttachedToWindow() -> onDetachFromWindow -> onRecycle + coroutineJobs += + coroutineScope.launch(dispatcherProvider.main) { + viewModel!!.state.collectLatest(::updateTaskViewState) + } + } + } + + private fun updateTaskViewState(state: TaskTileUiState) = + traceSection("TaskView.updateTaskViewState") { + sysUiStatusNavFlags = state.sysUiStatusNavFlags + + // Updating containers + val mapOfTasks = state.tasks.associateBy { it.taskId } + taskContainers.forEach { container -> + val taskId = container.task.key.id + val containerState = mapOfTasks[taskId] + val shouldHaveHeader = (type == TaskViewType.DESKTOP) && enableDesktopExplodedView() + container.setState( + state = containerState, + liveTile = state.isLiveTile, + hasHeader = shouldHaveHeader, + clickCloseListener = + if (shouldHaveHeader) { + { + // Update the layout UI to remove this task from the layout grid, + // and remove the task from ActivityManager afterwards. + recentsView?.dismissTask( + taskId, + /* animate= */ true, + /* removeTask= */ true, + ) + } + } else { + null + }, + ) + updateThumbnailValidity(container) + val thumbnailPosition = + updateThumbnailMatrix( + container = container, + width = container.thumbnailView.width, + height = container.thumbnailView.height, + ) + container.setOverlayEnabled(state.taskOverlayEnabled, thumbnailPosition) + if (state.isCentralTask) { + this.container.actionsView.let { + it.updateDisabledFlags( + DISABLED_ROTATED, + thumbnailPosition?.isRotated ?: false, + ) + it.updateDisabledFlags( + DISABLED_NO_THUMBNAIL, + state.tasks.any { taskData -> + (taskData as? TaskData.Data)?.thumbnailData?.thumbnail == null + }, + ) + } + } + + if (enableOverviewIconMenu()) { + setIconState(container, containerState) + } + } + } + + private fun updateThumbnailValidity(container: TaskContainer) { + container.isThumbnailValid = + viewModel?.isThumbnailValid( + thumbnail = container.thumbnailData, + width = container.thumbnailView.width, + height = container.thumbnailView.height, + ) ?: return + applyThumbnailSplashAlpha() + } + + /** + * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer. + * + * This function is called to reposition the thumbnail in the following scenarios: + * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash. + * - When drawing a snapshot (drawSnapshot). + * + * @param container The TaskContainer holding the thumbnail to be updated. + * @param width The desired width of the thumbnail's container. + * @param height The desired height of the thumbnail's container. + */ + private fun updateThumbnailMatrix( + container: TaskContainer, + width: Int, + height: Int, + ): ThumbnailPosition? = + traceSection("TaskView.updateThumbnailMatrix") { + val thumbnailPosition = + viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl) + ?: return null + container.updateThumbnailMatrix(thumbnailPosition.matrix) + return thumbnailPosition + } + + override fun onDetachedFromWindow() = + traceSection("TaskView.onDetachedFromWindow") { + super.onDetachedFromWindow() + if (enableRefactorTaskThumbnail()) { + // The jobs are being cancelled in the background thread. So we make a copy of the + // list to prevent cleaning a new job that might be added to this list during + // onAttach or another moment in the lifecycle. + val coroutineJobsToCancel = coroutineJobs.toList() + coroutineJobs.clear() + coroutineScope.launch(dispatcherProvider.background) { + traceSection("TaskView.onDetachedFromWindow.cancellingJobs") { + coroutineJobsToCancel.forEach { + it.cancel("TaskView detaching from window") + } + } + } + } + } + /** Updates this task view to the given {@param task}. */ open fun bind( task: Task, orientedState: RecentsOrientedState, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ) { cancelPendingLoadTasks() + this.orientedState = orientedState // Needed for dependencies taskContainers = listOf( createTaskContainer( @@ -664,69 +922,87 @@ constructor( R.id.snapshot, R.id.icon, R.id.show_windows, + R.id.digital_wellbeing_toast, STAGE_POSITION_UNDEFINED, - taskOverlayFactory + taskOverlayFactory, ) ) - setOrientationState(orientedState) + onBind(orientedState) } + protected open fun onBind(orientedState: RecentsOrientedState) = + traceSection("TaskView.onBind") { + traceSection("TaskView.onBind.createViewModel") { + if (enableRefactorTaskThumbnail()) { + val scopeId = context + Log.d(TAG, "onBind $scopeId ${orientedState.containerInterface}") + viewModel = + TaskViewModel( + taskViewType = type, + recentsViewData = RecentsDependencies.get(scopeId), + getTaskUseCase = RecentsDependencies.get(scopeId), + getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(scopeId), + isThumbnailValidUseCase = RecentsDependencies.get(scopeId), + getThumbnailPositionUseCase = RecentsDependencies.get(scopeId), + dispatcherProvider = RecentsDependencies.get(scopeId), + ) + .apply { bind(*taskIds) } + } + } + + taskContainers.forEach { container -> + container.bind() + if (enableRefactorTaskThumbnail()) { + container.thumbnailView.cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + container.thumbnailView.doOnSizeChange { width, height -> + updateThumbnailValidity(container) + val thumbnailPosition = updateThumbnailMatrix(container, width, height) + container.refreshOverlay(thumbnailPosition) + } + } + } + setOrientationState(orientedState) + } + + private fun applyThumbnailSplashAlpha() { + val alpha = getSplashAlphaProgress() + taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) } + } + + private fun getSplashAlphaProgress(): Float = + when { + !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha + splitSplashAlpha > 0f -> splitSplashAlpha + shouldShowSplash() -> taskThumbnailSplashAlpha + else -> 0f + } + + internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid } + protected fun createTaskContainer( task: Task, @IdRes thumbnailViewId: Int, @IdRes iconViewId: Int, @IdRes showWindowViewId: Int, + @IdRes digitalWellbeingBannerId: Int, @StagePosition stagePosition: Int, - taskOverlayFactory: TaskOverlayFactory - ): TaskContainer { - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!! - val thumbnailView: TaskThumbnailView? - if (enableRefactorTaskThumbnail()) { - val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated) - thumbnailView = - TaskThumbnailView(context).apply { - layoutParams = thumbnailViewDeprecated.layoutParams - addView(this, indexOfSnapshotView) - } - thumbnailViewDeprecated.visibility = GONE - } else { - thumbnailView = null - } - val iconView = getOrInflateIconView(iconViewId) - return TaskContainer( + taskOverlayFactory: TaskOverlayFactory, + ): TaskContainer = + traceSection("TaskView.createTaskContainer") { + val iconView = findViewById(iconViewId) as TaskViewIcon + return TaskContainer( + this, task, - thumbnailView, - thumbnailViewDeprecated, + findViewById(thumbnailViewId), iconView, TransformingTouchDelegate(iconView.asView()), stagePosition, - DigitalWellBeingToast(container, this), + findViewById(digitalWellbeingBannerId)!!, findViewById(showWindowViewId)!!, - taskOverlayFactory + taskOverlayFactory, ) - .apply { - if (enableRefactorTaskThumbnail()) { - thumbnailViewDeprecated.setTaskOverlay(overlay) - bindThumbnailView() - } else { - thumbnailViewDeprecated.bind(task, overlay) - } - } - } - - protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon { - val iconView = findViewById(iconViewId)!! - return iconView as? TaskViewIcon - ?: (iconView as ViewStub) - .apply { - layoutResource = - if (enableOverviewIconMenu()) R.layout.icon_app_chip_view - else R.layout.icon_view - } - .inflate() as TaskViewIcon - } - - protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized + } fun containsMultipleTasks() = taskContainers.size > 1 @@ -739,16 +1015,17 @@ constructor( /** Check if given `taskId` is tracked in this view */ fun containsTaskId(taskId: Int) = getTaskContainerById(taskId) != null - open fun setOrientationState(orientationState: RecentsOrientedState) { - this.orientedState = orientationState - taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) } - setThumbnailOrientation(orientationState) - } + open fun setOrientationState(orientationState: RecentsOrientedState) = + traceSection("TaskView.setOrientationState") { + this.orientedState = orientationState + taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) } + setThumbnailOrientation(orientationState) + } protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) { taskContainers.forEach { it.overlay.updateOrientationState(orientationState) - it.digitalWellBeingToast?.initialize(it.task) + it.digitalWellBeingToast?.initialize() } } @@ -756,11 +1033,7 @@ constructor( * Updates TaskView scaling and translation required to support variable width if enabled, while * ensuring TaskView fits into screen in fullscreen. */ - fun updateTaskSize( - lastComputedTaskSize: Rect, - lastComputedGridTaskSize: Rect, - lastComputedCarouselTaskSize: Rect - ) { + open fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) { val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx val taskWidth = lastComputedTaskSize.width() val taskHeight = lastComputedTaskSize.height() @@ -771,9 +1044,10 @@ constructor( if (container.deviceProfile.isTablet) { val boxWidth: Int val boxHeight: Int - if (isFocusedTask) { - // Task will be focused and should use focused task size. Use focusTaskRatio - // that is associated with the original orientation of the focused task. + + // Focused task and Desktop tasks should use focusTaskRatio that is associated + // with the original orientation of the focused task. + if (isLargeTile) { boxWidth = taskWidth boxHeight = taskHeight } else { @@ -787,22 +1061,15 @@ constructor( expectedHeight = boxHeight + thumbnailPadding // Scale to to fit task Rect. - nonGridScale = - if (enableGridOnlyOverview()) { - lastComputedCarouselTaskSize.width() / taskWidth.toFloat() - } else { - taskWidth / boxWidth.toFloat() - } + nonGridScale = taskWidth / boxWidth.toFloat() // Align to top of task Rect. boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f } else { nonGridScale = 1f boxTranslationY = 0f - expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT - expectedHeight = - if (enableOverviewIconMenu()) taskHeight + thumbnailPadding - else LayoutParams.MATCH_PARENT + expectedWidth = taskWidth + expectedHeight = taskHeight + thumbnailPadding } this.nonGridScale = nonGridScale this.boxTranslationY = boxTranslationY @@ -816,9 +1083,10 @@ constructor( protected open fun updateThumbnailSize() { // TODO(b/271468547), we should default to setting translations only on the snapshot instead // of a hybrid of both margins and translations - taskContainers[0].snapshotView.updateLayoutParams { + firstTaskContainer?.snapshotView?.updateLayoutParams { topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx } + taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() } } /** Returns the thumbnail's bounds, optionally relative to the screen. */ @@ -830,7 +1098,7 @@ constructor( if (relativeToDragLayer) { container.dragLayer.getDescendantRectRelativeToSelf( it.snapshotView, - thumbnailBounds + thumbnailBounds, ) } else { thumbnailBounds.set(it.snapshotView) @@ -862,7 +1130,8 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.thumbnailCache - .updateThumbnailInBackground(it.task) { thumbnailData -> + .getThumbnailInBackground(it.task) { thumbnailData -> + it.task.thumbnail = thumbnailData it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData) } ?.also { request -> pendingThumbnailLoadRequests.add(request) } @@ -874,39 +1143,65 @@ constructor( } } } - if (needsUpdate(changes, FLAG_UPDATE_ICON)) { + if (needsUpdate(changes, FLAG_UPDATE_ICON) && !enableOverviewIconMenu()) { taskContainers.forEach { if (visible) { recentsModel.iconCache - .updateIconInBackground(it.task) { task -> - setIcon(it.iconView, task.icon) - if (enableOverviewIconMenu()) { - setText(it.iconView, task.title) - } - it.digitalWellBeingToast?.initialize(task) + .getIconInBackground(it.task) { icon, contentDescription, title -> + it.task.icon = icon + it.task.titleDescription = contentDescription + it.task.title = title + onIconLoaded(it) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { - setIcon(it.iconView, null) - if (enableOverviewIconMenu()) { - setText(it.iconView, null) - } + onIconUnloaded(it) } } } if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { - currentFullscreenParams.updateCornerRadius(context) + thumbnailFullscreenParams.updateCornerRadius(context) } } protected open fun needsUpdate(@TaskDataChanges dataChange: Int, @TaskDataChanges flag: Int) = (dataChange and flag) == flag - protected open fun cancelPendingLoadTasks() { - pendingThumbnailLoadRequests.forEach { it.cancel() } - pendingThumbnailLoadRequests.clear() - pendingIconLoadRequests.forEach { it.cancel() } - pendingIconLoadRequests.clear() + protected open fun cancelPendingLoadTasks() = + traceSection("TaskView.cancelPendingLoadTasks") { + pendingThumbnailLoadRequests.forEach { it.cancel() } + pendingThumbnailLoadRequests.clear() + pendingIconLoadRequests.forEach { it.cancel() } + pendingIconLoadRequests.clear() + } + + protected open fun setIconState(container: TaskContainer, state: TaskData?) = + traceSection("TaskView.setIconState") { + if (enableOverviewIconMenu()) { + if (state is TaskData.Data) { + setIcon(container.iconView, state.icon) + container.iconView.setText(state.title) + container.digitalWellBeingToast?.initialize() + } else { + setIcon(container.iconView, null) + container.iconView.setText(null) + } + } + } + + protected open fun onIconLoaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, taskContainer.task.icon) + if (enableOverviewIconMenu()) { + taskContainer.iconView.setText(taskContainer.task.title) + } + taskContainer.digitalWellBeingToast?.initialize() + } + + protected open fun onIconUnloaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, null) + if (enableOverviewIconMenu()) { + taskContainer.iconView.setText(null) + } } protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) { @@ -930,13 +1225,14 @@ constructor( } } - protected fun setText(iconView: TaskViewIcon, text: CharSequence?) { - iconView.setText(text) - } - - open fun refreshThumbnails(thumbnailDatas: HashMap?) { + @JvmOverloads + open fun setShouldShowScreenshot( + shouldShowScreenshot: Boolean, + thumbnailDatas: Map? = null, + ) { + if (this.shouldShowScreenshot == shouldShowScreenshot) return + this.shouldShowScreenshot = shouldShowScreenshot if (enableRefactorTaskThumbnail()) { - // TODO(b/342560598) add thumbnail logic return } @@ -956,7 +1252,7 @@ constructor( return } val callbackList = - launchTasks()?.apply { + launchWithAnimation()?.apply { add { Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted") } @@ -964,79 +1260,180 @@ constructor( Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList") container.statsLogManager .logger() - .withItemInfo(firstItemInfo) + .withItemInfo(itemInfo) .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP) } + /** Launch of the current task (both live and inactive tasks) with an animation. */ + fun launchWithAnimation(): RunnableList? { + return if (isRunningTask && recentsView?.remoteTargetHandles != null) { + launchAsLiveTile(recentsView?.remoteTargetHandles!!) + } else { + launchAsStaticTile() + } + } + + private fun launchAsLiveTile(remoteTargetHandles: Array): RunnableList? { + val recentsView = recentsView ?: return null + if (!isClickableAsLiveTile) { + Log.e( + TAG, + "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}", + ) + return null + } + isClickableAsLiveTile = false + val targets = + if (remoteTargetHandles.isNotEmpty()) { + if (remoteTargetHandles.size == 1) { + remoteTargetHandles[0].transformParams.targetSet + } else { + val apps = + remoteTargetHandles.flatMap { + it.transformParams.targetSet.apps.asIterable() + } + val wallpapers = + remoteTargetHandles.flatMap { + it.transformParams.targetSet.wallpapers.asIterable() + } + RemoteAnimationTargets( + apps.toTypedArray(), + wallpapers.toTypedArray(), + remoteTargetHandles[0].transformParams.targetSet.nonApps, + remoteTargetHandles[0].transformParams.targetSet.targetMode, + ) + } + } else { + null + } + if (targets == null) { + // If the recents animation is cancelled somehow between the parent if block and + // here, try to launch the task as a non live tile task. + val runnableList = launchAsStaticTile() + if (runnableList == null) { + Log.e( + TAG, + "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}", + ) + } + isClickableAsLiveTile = true + return runnableList + } + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "composeRecentsLaunchAnimator", + taskIds.contentToString(), + ) + val runnableList = RunnableList() + with(AnimatorSet()) { + TaskViewUtils.composeRecentsLaunchAnimator( + this, + this@TaskView, + targets.apps, + targets.wallpapers, + targets.nonApps, + true, /* launcherClosing */ + recentsView.stateManager, + recentsView, + recentsView.depthController, + /* transitionInfo= */ null, + ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) { + launchAsStaticTile() + } + isClickableAsLiveTile = true + runEndCallback() + } + + override fun onAnimationCancel(animation: Animator) { + runEndCallback() + } + + private fun runEndCallback() { + runnableList.executeAllAndDestroy() + } + } + ) + start() + } + Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") + recentsView.onTaskLaunchedInLiveTileMode() + return runnableList + } + /** * Starts the task associated with this view and animates the startup. * * @return CompletionStage to indicate the animation completion or null if the launch failed. */ - open fun launchTaskAnimated(): RunnableList? { + open fun launchAsStaticTile(): RunnableList? { + val firstTaskContainer = firstTaskContainer ?: return null TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", - taskIds.contentToString() + taskIds.contentToString(), ) val opts = container.getActivityLaunchOptions(this, null).apply { - options.launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY + options.launchDisplayId = displayId } if ( ActivityManagerWrapper.getInstance() - .startActivityFromRecents(taskContainers[0].task.key, opts.options) + .startActivityFromRecents(firstTaskContainer.task.key, opts.options) ) { Log.d( TAG, - "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}" + "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}", ) ActiveGestureLog.INSTANCE.trackEvent( ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED ) val recentsView = recentsView ?: return null - if (recentsView.runningTaskViewId != -1) { + if ( + recentsView.runningTaskViewId != -1 && + recentsView.mRecentsAnimationController != null + ) { recentsView.onTaskLaunchedInLiveTileMode() // Return a fresh callback in the live tile case, so that it's not accidentally // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) } } - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - // If the recents transition is running (ie. in live tile mode), then the start - // of a new task will merge into the existing transition and it currently will - // not be run independently, so we need to rely on the onTaskAppeared() call - // for the new task to trigger the side launch callback to flush this runnable - // list (which is usually flushed when the app launch animation finishes) - recentsView.addSideTaskLaunchCallback(opts.onEndCallback) - } + // If the recents transition is running (ie. in live tile mode), then the start + // of a new task will merge into the existing transition and it currently will + // not be run independently, so we need to rely on the onTaskAppeared() call + // for the new task to trigger the side launch callback to flush this runnable + // list (which is usually flushed when the app launch animation finishes) + recentsView.addSideTaskLaunchCallback(opts.onEndCallback) return opts.onEndCallback } else { - notifyTaskLaunchFailed() + notifyTaskLaunchFailed("launchAsStaticTile") return null } } /** Starts the task associated with this view without any animation */ - fun launchTask(callback: (launched: Boolean) -> Unit) { - launchTask(callback, isQuickSwitch = false) - } - - /** Starts the task associated with this view without any animation */ - open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { + @JvmOverloads + open fun launchWithoutAnimation( + isQuickSwitch: Boolean = false, + callback: (launched: Boolean) -> Unit, + ) { + val firstTaskContainer = firstTaskContainer ?: return TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", - taskIds.contentToString() + taskIds.contentToString(), ) - val firstContainer = taskContainers[0] val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) if (isQuickSwitch) { // We only listen for failures to launch in quickswitch because the during this // gesture launcher is in the background state, vs other launches which are in // the actual overview state - failureListener.register(container, firstContainer.task.key.id) { - notifyTaskLaunchFailed() + failureListener.register(container, firstTaskContainer.task.key.id) { + notifyTaskLaunchFailed("launchWithoutAnimation") recentsView?.let { // Disable animations for now, as it is an edge case and the app usually // covers launcher and also any state transition animation also gets @@ -1058,7 +1455,7 @@ constructor( 0, 0, Executors.MAIN_EXECUTOR.handler, - { callback(true) } + { callback(true) }, ) { failureListener.onTransitionFinished() } @@ -1067,113 +1464,32 @@ constructor( if (isQuickSwitch) { setFreezeRecentTasksReordering() } - // TODO(b/334826842) add splash functionality to new TTV - if (!enableRefactorTaskThumbnail()) { - disableStartingWindow = - firstContainer.thumbnailViewDeprecated.shouldShowSplashView() - } + // TODO(b/331754864): Update this to use TV.shouldShowSplash + disableStartingWindow = firstTaskContainer.shouldShowSplashView } Executors.UI_HELPER_EXECUTOR.execute { if ( !ActivityManagerWrapper.getInstance() - .startActivityFromRecents(firstContainer.task.key, opts) + .startActivityFromRecents(firstTaskContainer.task.key, opts) ) { // If the call to start activity failed, then post the result immediately, // otherwise, wait for the animation start callback from the activity options // above Executors.MAIN_EXECUTOR.post { - notifyTaskLaunchFailed() + notifyTaskLaunchFailed("launchTask") callback(false) } } - Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}") + Log.d( + TAG, + "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}", + ) } } - /** Launch of the current task (both live and inactive tasks) with an animation. */ - fun launchTasks(): RunnableList? { - val recentsView = recentsView ?: return null - val remoteTargetHandles = recentsView.mRemoteTargetHandles - if (!isRunningTask || remoteTargetHandles == null) { - return launchTaskAnimated() - } - if (!isClickableAsLiveTile) { - Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.") - return null - } - isClickableAsLiveTile = false - val targets = - if (remoteTargetHandles.size == 1) { - remoteTargetHandles[0].transformParams.targetSet - } else { - val apps = - remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() } - val wallpapers = - remoteTargetHandles.flatMap { - it.transformParams.targetSet.wallpapers.asIterable() - } - RemoteAnimationTargets( - apps.toTypedArray(), - wallpapers.toTypedArray(), - remoteTargetHandles[0].transformParams.targetSet.nonApps, - remoteTargetHandles[0].transformParams.targetSet.targetMode - ) - } - if (targets == null) { - // If the recents animation is cancelled somehow between the parent if block and - // here, try to launch the task as a non live tile task. - val runnableList = launchTaskAnimated() - if (runnableList == null) { - Log.e( - TAG, - "Recents animation cancelled and cannot launch task as non-live tile" + - "; returning to home" - ) - } - isClickableAsLiveTile = true - return runnableList - } - val runnableList = RunnableList() - with(AnimatorSet()) { - TaskViewUtils.composeRecentsLaunchAnimator( - this, - this@TaskView, - targets.apps, - targets.wallpapers, - targets.nonApps, - true /* launcherClosing */, - recentsView.stateManager, - recentsView, - recentsView.depthController - ) - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animator: Animator) { - if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) { - launchTaskAnimated() - } - isClickableAsLiveTile = true - runEndCallback() - } - - override fun onAnimationCancel(animation: Animator) { - runEndCallback() - } - - private fun runEndCallback() { - runnableList.executeAllAndDestroy() - } - } - ) - start() - } - Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") - recentsView.onTaskLaunchedInLiveTileMode() - return runnableList - } - - private fun notifyTaskLaunchFailed() { - val sb = StringBuilder("Failed to launch task \n") + private fun notifyTaskLaunchFailed(launchMethod: String) { + val sb = + StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n") taskContainers.forEach { sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n") } @@ -1181,14 +1497,6 @@ constructor( Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show() } - fun initiateSplitSelect(splitPositionOption: SplitPositionOption) { - recentsView?.initiateSplitSelect( - this, - splitPositionOption.stagePosition, - SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition) - ) - } - /** * Returns `true` if user is already in split select mode and this tap was to choose the second * app. `false` otherwise @@ -1204,11 +1512,11 @@ constructor( this, container.task, container.iconView.drawable, - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, /* intent */ - null, /* user */ - null, - container.itemInfo + container.snapshotView, + container.thumbnail, + /* intent */ null, + /* user */ null, + container.itemInfo, ) } @@ -1237,12 +1545,38 @@ constructor( return showTaskMenuWithContainer(menuContainer) } + private fun closeTaskMenu(): Boolean { + val floatingView: AbstractFloatingView? = + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TASK_MENU, + ) + if (floatingView?.isOpen == true) { + floatingView.close(true) + return true + } else { + return false + } + } + private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean { val recentsView = recentsView ?: return false + if (enableHoverOfChildElementsInTaskview()) { + // Disable hover on all TaskView's whilst menu is showing. + recentsView.setTaskBorderEnabled(false) + } return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) { - menuContainer.iconView.revealAnim(/* isRevealing= */ true) - TaskMenuView.showForTask(menuContainer) { - menuContainer.iconView.revealAnim(/* isRevealing= */ false) + if (menuContainer.iconView.status == AppChipStatus.Expanded) { + closeTaskMenu() + } else { + menuContainer.iconView.revealAnim(/* isRevealing= */ true) + TaskMenuView.showForTask(menuContainer) { + val isAnimated = !recentsView.isSplitSelectionActive + menuContainer.iconView.revealAnim(/* isRevealing= */ false, isAnimated) + if (enableHoverOfChildElementsInTaskview()) { + recentsView.setTaskBorderEnabled(true) + } + } } } else if (container.deviceProfile.isTablet) { val alignedOptionIndex = @@ -1262,9 +1596,17 @@ constructor( } else { 0 } - TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) + TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) { + if (enableHoverOfChildElementsInTaskview()) { + recentsView.setTaskBorderEnabled(true) + } + } } else { - TaskMenuView.showForTask(menuContainer) + TaskMenuView.showForTask(menuContainer) { + if (enableHoverOfChildElementsInTaskview()) { + recentsView.setTaskBorderEnabled(true) + } + } } } @@ -1287,7 +1629,7 @@ constructor( private fun computeAndSetIconTouchDelegate( view: TaskViewIcon, tempCenterCoordinates: FloatArray, - transformingTouchDelegate: TransformingTouchDelegate + transformingTouchDelegate: TransformingTouchDelegate, ) { val viewHalfWidth = view.width / 2f val viewHalfHeight = view.height / 2f @@ -1298,13 +1640,13 @@ constructor( this[0] = viewHalfWidth this[1] = viewHalfHeight }, - false + false, ) transformingTouchDelegate.setBounds( (tempCenterCoordinates[0] - viewHalfWidth).toInt(), (tempCenterCoordinates[1] - viewHalfHeight).toInt(), (tempCenterCoordinates[0] + viewHalfWidth).toInt(), - (tempCenterCoordinates[1] + viewHalfHeight).toInt() + (tempCenterCoordinates[1] + viewHalfHeight).toInt(), ) } @@ -1314,7 +1656,7 @@ constructor( it.showWindowsView?.let { showWindowsView -> updateFilterCallback( showWindowsView, - getFilterUpdateCallback(it.task.key.packageName) + getFilterUpdateCallback(it.task.key.packageName), ) } } @@ -1347,23 +1689,23 @@ constructor( * Called to animate a smooth transition when going directly from an app into Overview (and vice * versa). Icons fade in, and DWB banners slide in with a "shift up" animation. */ - private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) { + private fun onSettledProgressUpdated(settledProgress: Float) { taskContainers.forEach { - it.iconView.setContentAlpha(focusTransitionProgress) - it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress) + it.iconView.setContentAlpha(settledProgress) + it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - settledProgress } } - fun animateIconScaleAndDimIntoView() { - iconAndDimAnimator?.cancel() - iconAndDimAnimator = - ObjectAnimator.ofFloat(focusTransitionScaleAndDim, MULTI_PROPERTY_VALUE, 0f, 1f).apply { - duration = SCALE_ICON_DURATION + fun startIconFadeInOnGestureComplete() { + iconFadeInOnGestureCompleteAnimator?.cancel() + iconFadeInOnGestureCompleteAnimator = + ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_GESTURE, 1f).apply { + duration = FADE_IN_ICON_DURATION interpolator = Interpolators.LINEAR addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - iconAndDimAnimator = null + iconFadeInOnGestureCompleteAnimator = null } } ) @@ -1371,20 +1713,21 @@ constructor( } } - fun setIconScaleAndDim(iconScale: Float) { - iconAndDimAnimator?.cancel() - focusTransitionScaleAndDim.value = iconScale + fun setIconVisibleForGesture(isVisible: Boolean) { + iconFadeInOnGestureCompleteAnimator?.cancel() + settledProgressGesture = if (isVisible) 1f else 0f } /** Set a color tint on the snapshot and supporting views. */ open fun setColorTint(amount: Float, tintColor: Int) { taskContainers.forEach { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334832108) Add scrim to new TTV + if (enableRefactorTaskThumbnail()) { + it.updateTintAmount(amount) + } else { it.thumbnailViewDeprecated.dimAlpha = amount } it.iconView.setIconColorTint(tintColor, amount) - it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount) + it.digitalWellBeingToast?.setColorTint(tintColor, amount) } } @@ -1398,7 +1741,7 @@ constructor( taskContainers.forEach { if (visibility == VISIBLE || it.task.key.id == taskId) { it.snapshotView.visibility = visibility - it.digitalWellBeingToast?.setBannerVisibility(visibility) + it.digitalWellBeingToast?.visibility = visibility it.showWindowsView?.visibility = visibility it.overlay.setVisibility(visibility) } @@ -1406,16 +1749,13 @@ constructor( } open fun setOverlayEnabled(overlayEnabled: Boolean) { - // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. - // and if it's still necessary we should support that in the new TTV class. if (!enableRefactorTaskThumbnail()) { - taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) } + taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) } } } protected open fun refreshTaskThumbnailSplash() { if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() } } } @@ -1423,27 +1763,15 @@ constructor( protected fun getScrollAdjustment(gridEnabled: Boolean) = if (gridEnabled) gridTranslationX else nonGridTranslationX - protected fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) + fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f private fun applyScale() { - val scale = persistentScale * dismissScale + val scale = persistentScale * dismissScale * Utilities.mapRange(modalness, 1f, modalScale) scaleX = scale scaleY = scale - if (enableRefactorTaskThumbnail()) { - taskViewData.scale.value = scale - } - updateSnapshotRadius() - } - - protected open fun applyThumbnailSplashAlpha() { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV - taskContainers.forEach { - it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha) - } - } + updateFullscreenParams() } private fun applyTranslationX() { @@ -1473,45 +1801,42 @@ constructor( protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) { taskContainers.forEach { - it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) + if (!enableOverviewIconMenu()) { + it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) + } it.overlay.setFullscreenProgress(fullscreenProgress) } - focusTransitionFullscreen.value = - FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress) - updateSnapshotRadius() + settledProgressFullscreen = + SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress) + updateFullscreenParams() } - protected open fun updateSnapshotRadius() { - updateCurrentFullscreenParams() + protected open fun updateFullscreenParams() { + updateFullscreenParams(thumbnailFullscreenParams) taskContainers.forEach { - it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams()) - it.overlay.setFullscreenParams(getThumbnailFullscreenParams()) + if (enableRefactorTaskThumbnail()) { + it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius + } else { + it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams) + } + it.overlay.setFullscreenParams(thumbnailFullscreenParams) } } - protected open fun updateCurrentFullscreenParams() { - updateFullscreenParams(currentFullscreenParams) - } - protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) { recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) } } - protected open fun getThumbnailFullscreenParams(): FullscreenDrawParams = - currentFullscreenParams - private fun onModalnessUpdated(modalness: Float) { + isClickable = modalness == 0f taskContainers.forEach { - it.iconView.setModalAlpha(1 - modalness) - it.digitalWellBeingToast?.updateBannerOffset(modalness) + it.iconView.setModalAlpha(1f - modalness) + it.digitalWellBeingToast?.bannerOffsetPercentage = modalness + } + if (enableGridOnlyOverview()) { + modalAlpha = if (isSelectedTask) 1f else (1f - modalness) + applyScale() } - } - - /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */ - fun notifyIsRunningTaskUpdated() { - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - taskContainers.forEach { it.bindThumbnailView() } } fun resetPersistentViewTransforms() { @@ -1519,11 +1844,14 @@ constructor( gridTranslationX = 0f gridTranslationY = 0f boxTranslationY = 0f - nonGridPivotTranslationX = 0f + taskContainers.forEach { + it.snapshotView.translationX = 0f + it.snapshotView.translationY = 0f + } resetViewTransforms() } - open fun resetViewTransforms() { + fun resetViewTransforms() { // fullscreenTranslation and accumulatedTranslation should not be reset, as // resetViewTransforms is called during QuickSwitch scrolling. dismissTranslationX = 0f @@ -1539,13 +1867,9 @@ constructor( } dismissScale = 1f translationZ = 0f - alpha = stableAlpha - setIconScaleAndDim(1f) + setIconVisibleForGesture(true) + settledProgressDismiss = 1f setColorTint(0f, 0) - if (!enableRefactorTaskThumbnail()) { - // TODO(b/335399428) add split select functionality to new TTV - taskContainers.forEach { it.thumbnailViewDeprecated.resetViewTransforms() } - } } private fun getGridTrans(endTranslation: Float) = @@ -1554,243 +1878,93 @@ constructor( private fun getNonGridTrans(endTranslation: Float) = endTranslation - getGridTrans(endTranslation) - /** We update and subsequently draw these in [fullscreenProgress]. */ - open class FullscreenDrawParams(context: Context) : SafeCloseable { - var cornerRadius = 0f - private var windowCornerRadius = 0f - var currentDrawnCornerRadius = 0f - - init { - updateCornerRadius(context) - } - - /** Recomputes the start and end corner radius for the given Context. */ - fun updateCornerRadius(context: Context) { - cornerRadius = computeTaskCornerRadius(context) - windowCornerRadius = computeWindowCornerRadius(context) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - open fun computeTaskCornerRadius(context: Context): Float { - return TaskCornerRadius.get(context) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - open fun computeWindowCornerRadius(context: Context): Float { - val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context) - - // The corner radius is fixed to match when Taskbar is persistent mode - return if ( - activityContext != null && - activityContext.deviceProfile?.isTaskbarPresent == true && - DisplayController.isTransientTaskbar(context) - ) { - context.resources - .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius) - .toFloat() - } else { - QuickStepContract.getWindowCornerRadius(context) - } - } - - /** Sets the progress in range [0, 1] */ - fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) { - currentDrawnCornerRadius = - Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) / - parentScale / - taskViewScale - } - - override fun close() {} + private fun MotionEvent.isWithinThumbnailBounds(): Boolean { + return thumbnailBounds.contains(x.toInt(), y.toInt()) } - /** Holder for all Task dependent information. */ - inner class TaskContainer( - val task: Task, - val thumbnailView: TaskThumbnailView?, - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated, - val iconView: TaskViewIcon, - /** - * This technically can be a vanilla [android.view.TouchDelegate] class, however that class - * requires setting the touch bounds at construction, so we'd repeatedly be created many - * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows - * touch delegated bounds only to be updated. - */ - val iconTouchDelegate: TransformingTouchDelegate, - /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ - @StagePosition val stagePosition: Int, - val digitalWellBeingToast: DigitalWellBeingToast?, - val showWindowsView: View?, - taskOverlayFactory: TaskOverlayFactory - ) { - val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this) - - val snapshotView: View - get() = thumbnailView ?: thumbnailViewDeprecated - - /** Builds proto for logging */ - val itemInfo: WorkspaceItemInfo - get() = - WorkspaceItemInfo().apply { - itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK - container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER - val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key) - user = componentKey.user - intent = Intent().setComponent(componentKey.componentName) - title = task.title - recentsView?.let { screenId = it.indexOfChild(this@TaskView) } - if (privateSpaceRestrictAccessibilityDrag()) { - if ( - UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate - ) { - runtimeStatusFlags = - runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE - } - } - } - - val taskView: TaskView - get() = this@TaskView - - fun destroy() { - digitalWellBeingToast?.destroy() - thumbnailView?.let { taskView.removeView(it) } - } - - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - fun bindThumbnailView() { - // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but - // this should be decided inside TaskThumbnailViewModel. - thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask)) + override fun addChildrenForAccessibility(outChildren: ArrayList) { + (if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach { + it.addChildForAccessibility(outChildren) } } companion object { private const val TAG = "TaskView" + + private enum class Alpha { + Stable, + Attach, + Split, + Modal, + } + + private enum class SettledProgress { + Fullscreen, + Gesture, + Dismiss, + } + const val FLAG_UPDATE_ICON = 1 const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1 const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1 const val FLAG_UPDATE_ALL = (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS) - const val FOCUS_TRANSITION_INDEX_FULLSCREEN = 0 - const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1 - const val FOCUS_TRANSITION_INDEX_COUNT = 2 - /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ const val MAX_PAGE_SCRIM_ALPHA = 0.4f - const val SCALE_ICON_DURATION: Long = 120 + const val FADE_IN_ICON_DURATION: Long = 120 private const val DIM_ANIM_DURATION: Long = 700 - private const val FOCUS_TRANSITION_THRESHOLD = - SCALE_ICON_DURATION.toFloat() / DIM_ANIM_DURATION - val FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR = + private const val SETTLE_TRANSITION_THRESHOLD = + FADE_IN_ICON_DURATION.toFloat() / DIM_ANIM_DURATION + val SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR = Interpolators.clampToProgress( Interpolators.FAST_OUT_SLOW_IN, - 1f - FOCUS_TRANSITION_THRESHOLD, - 1f + 1f - SETTLE_TRANSITION_THRESHOLD, + 1f, )!! + private val FADE_IN_ICON_INTERPOLATOR = Interpolators.LINEAR private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect()) - private val FOCUS_TRANSITION: FloatProperty = - object : FloatProperty("focusTransition") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.focusTransitionProgress = v - } + private val SETTLED_PROGRESS: FloatProperty = + KFloatProperty(TaskView::settledProgress) - override fun get(taskView: TaskView) = taskView.focusTransitionProgress - } + private val SETTLED_PROGRESS_GESTURE: FloatProperty = + KFloatProperty(TaskView::settledProgressGesture) + + private val SETTLED_PROGRESS_DISMISS: FloatProperty = + KFloatProperty(TaskView::settledProgressDismiss) private val SPLIT_SELECT_TRANSLATION_X: FloatProperty = - object : FloatProperty("splitSelectTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.splitSelectTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.splitSelectTranslationX - } + KFloatProperty(TaskView::splitSelectTranslationX) private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty = - object : FloatProperty("splitSelectTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.splitSelectTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.splitSelectTranslationY - } + KFloatProperty(TaskView::splitSelectTranslationY) private val DISMISS_TRANSLATION_X: FloatProperty = - object : FloatProperty("dismissTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.dismissTranslationX - } + KFloatProperty(TaskView::dismissTranslationX) private val DISMISS_TRANSLATION_Y: FloatProperty = - object : FloatProperty("dismissTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.dismissTranslationY - } + KFloatProperty(TaskView::dismissTranslationY) private val TASK_OFFSET_TRANSLATION_X: FloatProperty = - object : FloatProperty("taskOffsetTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskOffsetTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.taskOffsetTranslationX - } + KFloatProperty(TaskView::taskOffsetTranslationX) private val TASK_OFFSET_TRANSLATION_Y: FloatProperty = - object : FloatProperty("taskOffsetTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskOffsetTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.taskOffsetTranslationY - } + KFloatProperty(TaskView::taskOffsetTranslationY) private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty = - object : FloatProperty("taskResistanceTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskResistanceTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.taskResistanceTranslationX - } + KFloatProperty(TaskView::taskResistanceTranslationX) private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty = - object : FloatProperty("taskResistanceTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskResistanceTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.taskResistanceTranslationY - } + KFloatProperty(TaskView::taskResistanceTranslationY) @JvmField val GRID_END_TRANSLATION_X: FloatProperty = - object : FloatProperty("gridEndTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.gridEndTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.gridEndTranslationX - } + KFloatProperty(TaskView::gridEndTranslationX) @JvmField - val DISMISS_SCALE: FloatProperty = - object : FloatProperty("dismissScale") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissScale = v - } + val DISMISS_SCALE: FloatProperty = KFloatProperty(TaskView::dismissScale) - override fun get(taskView: TaskView) = taskView.dismissScale - } + @JvmField val SPLIT_ALPHA: FloatProperty = KFloatProperty(TaskView::splitAlpha) } } diff --git a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java index 94739cbc9..80e3a2b93 100644 --- a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java +++ b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java @@ -47,6 +47,11 @@ public interface TaskViewIcon { */ void setModalAlpha(float alpha); + /** + * Sets the opacity of the view for flex split state. + */ + void setFlexSplitAlpha(float alpha); + /** * Returns this icon view's drawable. */ diff --git a/quickstep/src/com/android/quickstep/views/TaskViewType.kt b/quickstep/src/com/android/quickstep/views/TaskViewType.kt new file mode 100644 index 000000000..b2a32a96c --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskViewType.kt @@ -0,0 +1,24 @@ +/* + * 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.quickstep.views + +/** Type of the [TaskView] */ +enum class TaskViewType { + SINGLE, + GROUPED, + DESKTOP +} diff --git a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java new file mode 100644 index 000000000..cb7254f1f --- /dev/null +++ b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java @@ -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.launcher3.util; + +import static com.android.quickstep.util.QuickstepProtoLogGroup.LAUNCHER_STATE_MANAGER; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.window.DesktopModeFlags.DesktopModeFlag; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.launcher3.Flags; + +/** + * Proxy class used for StateManager ProtoLog support. + */ +public class StateManagerProtoLogProxy { + private static final DesktopModeFlag ENABLE_STATE_MANAGER_PROTO_LOG = + new DesktopModeFlag(Flags::enableStateManagerProtoLog, true); + public static void logGoToState( + @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, + "StateManager.goToState: fromState: %s, toState: %s, partial trace:\n%s", + fromState, + toState, + trace); + } + + public static void logCreateAtomicAnimation( + @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.createAtomicAnimation: " + + "fromState: %s, toState: %s, partial trace:\n%s", + fromState, + toState, + trace); + } + + public static void logOnStateTransitionStart(@NonNull Object state) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionStart: state: %s", state); + } + + public static void logOnStateTransitionEnd(@NonNull Object state) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionEnd: state: %s", state); + } + + public static void logCancelAnimation(boolean animationOngoing, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, + "StateManager.cancelAnimation: animation ongoing: %b, partial trace:\n%s", + animationOngoing, + trace); + } +} diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java similarity index 94% rename from quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java index cfa6b9891..ab1097992 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.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. @@ -40,6 +40,7 @@ public class ActiveGestureErrorDetector { SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED, FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING, INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING, + QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED, /** * These GestureEvents are specifically associated to state flags that get set in @@ -282,6 +283,29 @@ public class ActiveGestureErrorDetector { + " animation is still pending.", writer); break; + case QUICK_SWITCH_FROM_HOME_FALLBACK: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Quick switch from home fallback case: the " + + "TaskView at the current page index was missing.", + writer); + break; + case QUICK_SWITCH_FROM_HOME_FAILED: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Quick switch from home failed: the TaskViews at " + + "the current page index and index 0 were missing.", + writer); + break; + case NAVIGATION_MODE_SWITCHED: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Navigation mode switched mid-gesture.", + writer); + break; case EXPECTING_TASK_APPEARED: case MOTION_DOWN: case SET_END_TARGET: diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java similarity index 77% rename from quickstep/src/com/android/quickstep/util/ActiveGestureLog.java rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java index c54862aba..23e245c51 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 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. @@ -18,11 +18,11 @@ package com.android.quickstep.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.util.Preconditions; - import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -71,14 +71,6 @@ public class ActiveGestureLog { addLog(event, null); } - public void addLog(@NonNull String event, int extras) { - addLog(event, extras, null); - } - - public void addLog(@NonNull String event, boolean extras) { - addLog(event, extras, null); - } - /** * Adds a log to be printed at log-dump-time and track the associated event for error detection. * @@ -89,20 +81,6 @@ public class ActiveGestureLog { addLog(new CompoundString(event), gestureEvent); } - public void addLog( - @NonNull String event, - int extras, - @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { - addLog(new CompoundString(event).append(": ").append(extras), gestureEvent); - } - - public void addLog( - @NonNull String event, - boolean extras, - @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { - addLog(new CompoundString(event).append(": ").append(extras), gestureEvent); - } - public void addLog(@NonNull CompoundString compoundString) { addLog(compoundString, null); } @@ -237,7 +215,8 @@ public class ActiveGestureLog { /** An entire log of entries associated with a single log ID */ protected static class EventLog { - protected final List eventEntries = new ArrayList<>(); + protected final List eventEntries = + Collections.synchronizedList(new ArrayList<>()); protected final int logId; protected final boolean mIsFullyGesturalNavMode; @@ -250,25 +229,27 @@ public class ActiveGestureLog { /** A buildable string stored as an array for memory efficiency. */ public static class CompoundString { - public static final CompoundString NO_OP = new CompoundString(); + public static final CompoundString NO_OP = new CompoundString(true); private final List mSubstrings; private final List mArgs; private final boolean mIsNoOp; - private CompoundString() { - this(null); + public static CompoundString newEmptyString() { + return new CompoundString(false); } - public CompoundString(String substring) { - mIsNoOp = substring == null; + private CompoundString(boolean isNoOp) { + mIsNoOp = isNoOp; mSubstrings = mIsNoOp ? null : new ArrayList<>(); mArgs = mIsNoOp ? null : new ArrayList<>(); + } - if (!mIsNoOp) { - mSubstrings.add(substring); - } + public CompoundString(String substring, Object... args) { + this(substring == null); + + append(substring, args); } public CompoundString append(CompoundString substring) { @@ -281,80 +262,24 @@ public class ActiveGestureLog { return this; } - public CompoundString append(String substring) { + public CompoundString append(String substring, Object... args) { if (mIsNoOp) { return this; } mSubstrings.add(substring); + mArgs.addAll(Arrays.stream(args).toList()); return this; } - public CompoundString append(int num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%d"); - } - - public CompoundString append(long num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%d"); - } - - public CompoundString append(float num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%.2f"); - } - - public CompoundString append(double num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%.2f"); - } - - public CompoundString append(boolean bool) { - if (mIsNoOp) { - return this; - } - mArgs.add(bool); - - return append("%b"); - } - - private Object[] getArgs() { - Preconditions.assertTrue(!mIsNoOp); - - return mArgs.toArray(); - } - @Override public String toString() { - return String.format(toUnformattedString(), getArgs()); - } - - private String toUnformattedString() { - Preconditions.assertTrue(!mIsNoOp); - + if (mIsNoOp) return null; StringBuilder sb = new StringBuilder(); for (String substring : mSubstrings) { sb.append(substring); } - - return sb.toString(); + return String.format(sb.toString(), mArgs.toArray()); } @Override @@ -364,10 +289,9 @@ public class ActiveGestureLog { @Override public boolean equals(Object obj) { - if (!(obj instanceof CompoundString)) { + if (!(obj instanceof CompoundString other)) { return false; } - CompoundString other = (CompoundString) obj; return (mIsNoOp == other.mIsNoOp) && Objects.equals(mSubstrings, other.mSubstrings) && Objects.equals(mArgs, other.mArgs); diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java new file mode 100644 index 000000000..1c8656c54 --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java @@ -0,0 +1,582 @@ +/* + * 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.quickstep.util; + +import static android.view.MotionEvent.ACTION_DOWN; + +import static com.android.launcher3.Flags.enableActiveGestureProtoLog; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; +import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.graphics.Point; +import android.graphics.RectF; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; + +/** + * Proxy class used for ActiveGestureLog ProtoLog support. + *

+ * This file will have all of its static strings in the + * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings. + *

+ * When a new ActiveGestureLog entry needs to be added to the codebase (or and existing entry needs + * to be modified), add it here under a new unique method and make sure the ProtoLog entry matches + * to avoid confusion. + */ +public class ActiveGestureProtoLogProxy { + + public static void logLauncherDestroyed() { + ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED); + if (isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed"); + } + + public static void logAbsSwipeUpHandlerOnRecentsAnimationCanceled() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "AbsSwipeUpHandler.onRecentsAnimationCanceled", + /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onRecentsAnimationCanceled"); + } + + public static void logAbsSwipeUpHandlerOnRecentsAnimationFinished() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "RecentsAnimationCallbacks.onAnimationFinished", + ON_FINISH_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onAnimationFinished"); + } + + public static void logAbsSwipeUpHandlerCancelCurrentAnimation() { + ActiveGestureLog.INSTANCE.addLog( + "AbsSwipeUpHandler.cancelCurrentAnimation", + ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.cancelCurrentAnimation"); + } + + public static void logAbsSwipeUpHandlerOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.onTasksAppeared: " + + "force finish recents animation complete; clearing state callback."); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onTasksAppeared: " + + "force finish recents animation complete; clearing state callback."); + } + + public static void logHandOffAnimation() { + ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation"); + } + + public static void logFinishRecentsAnimationOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimationOnTasksAppeared"); + } + + public static void logRecentsAnimationCallbacksOnAnimationCancelled() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled", + /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationCanceled"); + } + + public static void logRecentsAnimationCallbacksOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared", + ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onTasksAppeared"); + } + + public static void logStartRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "TaskAnimationManager.startRecentsAnimation", + /* gestureEvent= */ START_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager.startRecentsAnimation"); + } + + public static void logLaunchingSideTaskFailed() { + ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Unable to launch side task (no recents)"); + } + + public static void logContinueRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "continueRecentsAnimation"); + } + + public static void logCleanUpRecentsAnimationSkipped() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation skipped due to wrong callbacks"); + } + + public static void logCleanUpRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation"); + } + + public static void logOnInputEventUserLocked(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: user is locked", + displayId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: user is locked", + displayId); + } + + public static void logOnInputIgnoringFollowingEvents(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onMotionEvent(displayId=%d): A new gesture has been started, " + + "but a previously-requested recents animation hasn't started. " + + "Ignoring all following motion events.", displayId), + RECENTS_ANIMATION_START_PENDING); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onMotionEvent(displayId=%d): A new gesture has been started, " + + "but a previously-requested recents animation hasn't started. " + + "Ignoring all following motion events.", displayId); + } + + public static void logOnInputEventThreeButtonNav(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "using 3-button nav and event is not a trackpad event", displayId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "using 3-button nav and event is not a trackpad event", displayId); + } + + public static void logPreloadRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "preloadRecentsAnimation"); + } + + public static void logRecentTasksMissing() { + ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Null mRecentTasks"); + } + + public static void logExecuteHomeCommand() { + ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewCommandHelper.executeCommand(HOME)"); + } + + public static void logFinishRecentsAnimationCallback() { + ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback"); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation-callback"); + } + + public static void logOnScrollerAnimationAborted() { + ActiveGestureLog.INSTANCE.addLog("scroller animation aborted", + ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "scroller animation aborted"); + } + + public static void logInputConsumerBecameActive(@NonNull String consumerName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "%s became active", consumerName)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "%s became active", consumerName); + } + + public static void logTaskLaunchFailed(int launchedTaskId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launch failed, task (id=%d) finished mid transition", launchedTaskId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Launch failed, task (id=%d) finished mid transition", launchedTaskId); + } + + public static void logOnPageEndTransition(int nextPageIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onPageEndTransition: current page index updated: %d", nextPageIndex)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onPageEndTransition: current page index updated: %d", nextPageIndex); + } + + public static void logQuickSwitchFromHomeFallback(int taskIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home fallback case: The TaskView at index %d is missing.", + taskIndex), + QUICK_SWITCH_FROM_HOME_FALLBACK); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Quick switch from home fallback case: The TaskView at index %d is missing.", + taskIndex); + } + + public static void logQuickSwitchFromHomeFailed(int taskIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home failed: TaskViews at indices %d and 0 are missing.", + taskIndex), + QUICK_SWITCH_FROM_HOME_FAILED); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Quick switch from home failed: TaskViews at indices %d and 0 are missing.", + taskIndex); + } + + public static void logFinishRecentsAnimation(boolean toRecents) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "finishRecentsAnimation: %b", toRecents), + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation: %b", toRecents); + } + + public static void logSetEndTarget(@NonNull String target) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "setEndTarget %s", target), /* gestureEvent= */ SET_END_TARGET); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "setEndTarget %s", target); + } + + public static void logStartHomeIntent(@NonNull String reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "OverviewComponentObserver.startHomeIntent: %s", reason)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewComponentObserver.startHomeIntent: %s", reason); + } + + public static void logRunningTaskPackage(@NonNull String packageName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Current running task package name=%s", packageName)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Current running task package name=%s", packageName); + } + + public static void logSysuiStateFlags(@NonNull String stateFlags) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Current SystemUi state flags=%s", stateFlags)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Current SystemUi state flags=%s", stateFlags); + } + + public static void logSetInputConsumer(@NonNull String consumerName, @NonNull String reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "setInputConsumer: %s. reason(s):%s", consumerName, reason)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "setInputConsumer: %s. reason(s):%s", consumerName, reason); + } + + public static void logUpdateGestureStateRunningTask( + @NonNull String otherTaskPackage, @NonNull String runningTaskPackage) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Changing active task to %s because the previous task running on top of this " + + "one (%s) was excluded from recents", + otherTaskPackage, + runningTaskPackage)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Changing active task to %s because the previous task running on top of this " + + "one (%s) was excluded from recents", + otherTaskPackage, + runningTaskPackage); + } + + public static void logOnInputEventActionUp( + int x, int y, int action, @NonNull String classification, int displayId) { + String actionString = MotionEvent.actionToString(action); + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent(%d, %d): %s, %s, displayId=%d", + x, + y, + actionString, + classification, + displayId), + /* gestureEvent= */ action == ACTION_DOWN + ? MOTION_DOWN + : MOTION_UP); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent(%d, %d): %s, %s, displayId=%d", + x, + y, + actionString, + classification, + displayId); + } + + public static void logOnInputEventActionMove( + @NonNull String action, + @NonNull String classification, + int pointerCount, + int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent: %s, %s, pointerCount: %d, displayId=%d", + action, + classification, + pointerCount, + displayId), + MOTION_MOVE); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent: %s, %s, pointerCount: %d, displayId=%d", + action, + classification, + pointerCount, + displayId); + } + + public static void logOnInputEventGenericAction( + @NonNull String action, @NonNull String classification, int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent: %s, %s, displayId=%d", action, classification, displayId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent: %s, %s, displayId=%d", action, classification, displayId); + } + + public static void logOnInputEventNavModeSwitched( + int displayId, @NonNull String startNavMode, @NonNull String currentNavMode) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Navigation mode switched mid-gesture (%s -> %s); " + + "cancelling gesture.", + displayId, + startNavMode, + currentNavMode), + NAVIGATION_MODE_SWITCHED); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Navigation mode switched mid-gesture (%s -> %s); " + + "cancelling gesture.", + displayId, + startNavMode, + currentNavMode); + } + + public static void logUnknownInputEvent(int displayId, @NonNull String event) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "received unknown event %s", displayId, event)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "received unknown event %s", displayId, event); + } + + public static void logFinishRunningRecentsAnimation(boolean toHome) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "finishRunningRecentsAnimation: %b", toHome)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRunningRecentsAnimation: %b", toHome); + } + + public static void logOnRecentsAnimationStartCancelled() { + ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onAnimationStart (canceled): 0", + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationStart (canceled): 0"); + } + + public static void logOnRecentsAnimationStart(int appCount) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "RecentsAnimationCallbacks.onAnimationStart: %d", appCount), + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "RecentsAnimationCallbacks.onAnimationStart: %d", appCount); + } + + public static void logStartRecentsAnimationCallback(@NonNull String callback) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager.startRecentsAnimation(%s): " + + "Setting mRecentsAnimationStartPending = false", + callback)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TaskAnimationManager.startRecentsAnimation(%s): " + + "Setting mRecentsAnimationStartPending = false", + callback); + } + + public static void logSettingRecentsAnimationStartPending(boolean value) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager.startRecentsAnimation: " + + "Setting mRecentsAnimationStartPending = %b", + value)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TaskAnimationManager.startRecentsAnimation: " + + "Setting mRecentsAnimationStartPending = %b", + value); + } + + public static void logLaunchingSideTask(int taskId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launching side task id=%d", taskId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launching side task id=%d", taskId); + } + + public static void logOnInputEventActionDown( + int displayId, @NonNull ActiveGestureLog.CompoundString reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onMotionEvent(displayId=%d): ", displayId).append(reason)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onMotionEvent(displayId=%d): %s", displayId, reason.toString()); + } + + public static void logStartNewTask(@NonNull ActiveGestureLog.CompoundString tasks) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launching task: ").append(tasks)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", tasks.toString()); + } + + public static void logMotionPauseDetectorEvent(@NonNull ActiveGestureLog.CompoundString event) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "MotionPauseDetector: ").append(event)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "MotionPauseDetector: %s", event.toString()); + } + + public static void logHandleTaskAppearedFailed( + @NonNull ActiveGestureLog.CompoundString reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "handleTaskAppeared check failed: ").append(reason)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "handleTaskAppeared check failed: %s", reason.toString()); + } + + /** + * This is for special cases where the string is purely dynamic and therefore has no format that + * can be extracted. Do not use in any other case. + */ + public static void logDynamicString( + @NonNull String string, + @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + ActiveGestureLog.INSTANCE.addLog(string, gestureEvent); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "%s", string); + } + + public static void logOnSettledOnEndTarget(@NonNull String endTarget) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onSettledOnEndTarget %s", endTarget), + /* gestureEvent= */ ON_SETTLED_ON_END_TARGET); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "onSettledOnEndTarget %s", endTarget); + } + + public static void logOnCalculateEndTarget(float velocityX, float velocityY, double angle) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f", + velocityX, + velocityY, + angle), + velocityX == 0 && velocityY == 0 ? INVALID_VELOCITY_ON_SWIPE_UP : null); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f", + velocityX, + velocityY, + angle); + } + + public static void logUnexpectedTaskAppeared(int taskId, @NonNull String packageName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s", + taskId, + packageName)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s", + taskId, + packageName); + } + + public static void logCreateTouchRegionForDisplay(int displayRotation, + @NonNull Point displaySize, @NonNull RectF swipeRegion, @NonNull RectF ohmRegion, + int gesturalHeight, int largerGesturalHeight, @NonNull String reason) { + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "OrientationTouchTransformer.createRegionForDisplay: " + + "dispRot=%d, dispSize=%s, swipeRegion=%s, ohmRegion=%s, " + + "gesturalHeight=%d, largerGesturalHeight=%d, reason=%s", + displayRotation, displaySize.flattenToString(), swipeRegion.toShortString(), + ohmRegion.toShortString(), gesturalHeight, largerGesturalHeight, reason); + } + + public static void logOnTaskAnimationManagerNotAvailable(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager not available for displayId=%d", + displayId)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager not available for displayId=%d", + displayId); + } + + public static void logGestureStartSwipeHandler(@NonNull String interactionHandler) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation: " + + "interactionHandler=%s", interactionHandler)); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation: " + + "interactionHandler=%s", interactionHandler); + } + + public static void logQueuingForceFinishRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog("Launcher destroyed while mRecentsAnimationStartPending ==" + + " true, queuing a callback to clean the pending animation up on start", + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed while mRecentsAnimationStartPending ==" + + " true, queuing a callback to clean the pending animation up on start"); + } +} diff --git a/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java new file mode 100644 index 000000000..2327cfcf1 --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java @@ -0,0 +1,133 @@ +/* + * 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.quickstep.util; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; + +import java.util.UUID; + +/** Enums used to interface with the ProtoLog API. */ +public enum QuickstepProtoLogGroup implements IProtoLogGroup { + + ACTIVE_GESTURE_LOG(true, true, Constants.DEBUG_ACTIVE_GESTURE, "ActiveGestureLog"), + RECENTS_WINDOW(true, true, Constants.DEBUG_RECENTS_WINDOW, "RecentsWindow"), + LAUNCHER_STATE_MANAGER(true, true, Constants.DEBUG_STATE_MANAGER, "LauncherStateManager"); + + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final @NonNull String mTag; + + public static boolean isProtoLogInitialized() { + if (!Variables.sIsInitialized) { + Log.w(Constants.TAG, + "Attempting to log to ProtoLog before initializing it.", + new IllegalStateException()); + } + return Variables.sIsInitialized; + } + + public static void initProtoLog() { + if (Variables.sIsInitialized) { + Log.e(Constants.TAG, + "Attempting to re-initialize ProtoLog.", new IllegalStateException()); + return; + } + Log.i(Constants.TAG, "Initializing ProtoLog."); + Variables.sIsInitialized = true; + ProtoLog.init(QuickstepProtoLogGroup.values()); + } + + /** + * @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 + */ + QuickstepProtoLogGroup( + boolean enabled, boolean logToProto, boolean logToLogcat, @NonNull 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 int getId() { + return Constants.LOG_START_ID + this.ordinal(); + } + + @Override + public @NonNull String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + private static final class Variables { + + private static boolean sIsInitialized = false; + } + + private static final class Constants { + + private static final String TAG = "QuickstepProtoLogGroup"; + + private static final boolean DEBUG_ACTIVE_GESTURE = false; + private static final boolean DEBUG_RECENTS_WINDOW = false; + private static final boolean DEBUG_STATE_MANAGER = true; // b/279059025, b/325463989 + + private static final int LOG_START_ID = + (int) (UUID.nameUUIDFromBytes(QuickstepProtoLogGroup.class.getName().getBytes()) + .getMostSignificantBits() % Integer.MAX_VALUE); + } +} diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java new file mode 100644 index 000000000..99888fb2a --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java @@ -0,0 +1,64 @@ +/* + * 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.quickstep.util; + +import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.window.DesktopModeFlags; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.launcher3.Flags; + +/** + * Proxy class used for Recents Window ProtoLog support. + *

+ * This file will have all of its static strings in the + * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings. + *

+ * When a new Recents Window log needs to be added to the codebase, add it here under a new unique + * method. Or, if an existing entry needs to be modified, simply update it here. + */ +public class RecentsWindowProtoLogProxy { + private static final DesktopModeFlags.DesktopModeFlag ENABLE_RECENTS_WINDOW_PROTO_LOG = + new DesktopModeFlags.DesktopModeFlag(Flags::enableRecentsWindowProtoLog, true); + public static void logOnStateSetStart(@NonNull String stateName) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName); + } + + public static void logOnStateSetEnd(@NonNull String stateName) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName); + } + + public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, + "Starting recents window: isShow= %b, windowViewIsNull=%b", + isShown, + windowViewIsNull); + } + + public static void logCleanup(boolean isShown) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown); + } +} diff --git a/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt new file mode 100644 index 000000000..2f1f0b5f6 --- /dev/null +++ b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt @@ -0,0 +1,99 @@ +/* + * 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.launcher3.taskbar.bubbles.testing + +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.PathParser +import android.view.LayoutInflater +import android.view.ViewGroup +import com.android.launcher3.R +import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.wm.shell.shared.bubbles.BubbleInfo + +object FakeBubbleViewFactory { + + /** Inflates a [BubbleView] and adds it to the [parent] view if it is present. */ + fun createBubble( + context: Context, + key: String, + parent: ViewGroup?, + iconSize: Int = 50, + iconColor: Int, + badgeColor: Int = Color.RED, + dotColor: Int = Color.BLUE, + suppressNotification: Boolean = false, + ): BubbleView { + val inflater = LayoutInflater.from(context) + // BubbleView uses launcher's badge to icon ratio and expects the badge image to already + // have the right size + val badgeToIconRatio = 0.444f + val badgeRadius = iconSize * badgeToIconRatio / 2 + val icon = createCircleBitmap(radius = iconSize / 2, color = iconColor) + val badge = createCircleBitmap(radius = badgeRadius.toInt(), color = badgeColor) + + val flags = + if (suppressNotification) Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION else 0 + val bubbleInfo = + BubbleInfo( + key, + flags, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + val bubbleView = inflater.inflate(R.layout.bubblebar_item_view, parent, false) as BubbleView + val dotPath = + PathParser.createPathFromPathData( + context.resources.getString(com.android.internal.R.string.config_icon_mask) + ) + val bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + badge, + icon, + dotColor, + dotPath, + "test app", + null, + ) + bubbleView.setBubble(bubble) + return bubbleView + } + + private fun createCircleBitmap(radius: Int, color: Int): Bitmap { + val bitmap = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawARGB(0, 0, 0, 0) + val paint = Paint() + paint.color = color + canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paint) + return bitmap + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt new file mode 100644 index 000000000..b5a418b04 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import android.platform.test.rule.ScreenRecordRule +import android.view.View +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT +import androidx.activity.ComponentActivity +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.R +import com.android.launcher3.taskbar.bubbles.testing.FakeBubbleViewFactory +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** Screenshot tests for [BubbleBarView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +@ScreenRecordRule.ScreenRecord +class BubbleBarViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleBarView_collapsed_oneBubble() { + screenshotRule.screenshotTest("bubbleBarView_collapsed_oneBubble") { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN)) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + container + } + } + + @Test + fun bubbleBarView_collapsed_twoBubbles() { + screenshotRule.screenshotTest("bubbleBarView_collapsed_twoBubbles") { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN)) + bubbleBarView.addBubble(createBubble("key2", Color.CYAN)) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + container + } + } + + @Test + fun bubbleBarView_expanded_threeBubbles() { + // if we're still expanding, wait with taking a screenshot + val shouldWait: (ComponentActivity, View) -> Boolean = { _, _ -> bubbleBarView.isExpanding } + // increase the frame limit to allow the animation to end before taking the screenshot + screenshotRule.frameLimit = 500 + screenshotRule.screenshotTest( + "bubbleBarView_expanded_threeBubbles", + checkView = shouldWait, + ) { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN)) + bubbleBarView.addBubble(createBubble("key2", Color.CYAN)) + bubbleBarView.addBubble(createBubble("key3", Color.MAGENTA)) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + bubbleBarView.isExpanded = true + container + } + } + + private fun setupBubbleBarView() { + bubbleBarView = BubbleBarView(context) + val lp = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + bubbleBarView.layoutParams = lp + val paddingTop = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size) + bubbleBarView.setPadding(0, paddingTop, 0, 0) + bubbleBarView.visibility = View.VISIBLE + bubbleBarView.alpha = 1f + } + + private fun createBubble(key: String, color: Int): BubbleView { + val bubbleView = + FakeBubbleViewFactory.createBubble( + context, + key, + parent = bubbleBarView, + iconColor = color, + ) + bubbleView.showDotIfNeeded(1f) + bubbleBarView.setSelectedBubble(bubbleView) + return bubbleView + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt new file mode 100644 index 000000000..47f393ffc --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt @@ -0,0 +1,94 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.taskbar.bubbles.testing.FakeBubbleViewFactory +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** Screenshot tests for [BubbleView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleView_hasUnseenContent() { + screenshotRule.screenshotTest("bubbleView_hasUnseenContent") { activity -> + activity.actionBar?.hide() + setupBubbleView() + } + } + + @Test + fun bubbleView_seen() { + screenshotRule.screenshotTest("bubbleView_seen") { activity -> + activity.actionBar?.hide() + setupBubbleView(suppressNotification = true) + } + } + + @Test + fun bubbleView_badgeHidden() { + screenshotRule.screenshotTest("bubbleView_badgeHidden") { activity -> + activity.actionBar?.hide() + setupBubbleView().apply { setBadgeScale(0f) } + } + } + + private fun setupBubbleView(suppressNotification: Boolean = false): BubbleView { + val bubbleView = + FakeBubbleViewFactory.createBubble( + context, + key = "key", + parent = null, + iconSize = 100, + iconColor = Color.LTGRAY, + suppressNotification = suppressNotification, + ) + bubbleView.showDotIfNeeded(1f) + return bubbleView + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS new file mode 100644 index 000000000..63c14984b --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com +mpodolian@google.com diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt new file mode 100644 index 000000000..11c7fe982 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt @@ -0,0 +1,253 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.content.Context +import android.graphics.Color +import android.graphics.PointF +import android.graphics.drawable.ColorDrawable +import androidx.test.core.app.ApplicationProvider +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** Screenshot tests for [BubbleBarFlyoutView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleBarFlyoutView_noAvatar_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message") + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message") + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = null, + title = "sender", + message = "really, really, really, really, really long message. like really.", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "message", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "message", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "really, really, really, really, really long message. like really.", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_collapsed_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "collapsed on left", + ) + ) {} + flyout.updateExpansionProgress(0f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_collapsed_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "collapsed on right", + ) + ) {} + flyout.updateExpansionProgress(0f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_90p_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_90p_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView( + context, + FakeBubbleBarFlyoutPositioner( + isOnLeft = true, + distanceToCollapsedPosition = PointF(100f, 100f), + ), + ) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "expanded 90% on left", + ) + ) {} + flyout.updateExpansionProgress(0.9f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_80p_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_80p_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView( + context, + FakeBubbleBarFlyoutPositioner( + isOnLeft = false, + distanceToCollapsedPosition = PointF(200f, 100f), + ), + ) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "expanded 80% on right", + ) + ) {} + flyout.updateExpansionProgress(0.8f) + flyout + } + } + + private class FakeBubbleBarFlyoutPositioner( + override val isOnLeft: Boolean, + override val distanceToCollapsedPosition: PointF = PointF(0f, 0f), + ) : BubbleBarFlyoutPositioner { + override val targetTy = 0f + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 10f + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt new file mode 100644 index 000000000..80b2c16e1 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt @@ -0,0 +1,272 @@ +/* + * 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.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable +import android.view.LayoutInflater +import android.view.Surface.ROTATION_0 +import androidx.core.graphics.set +import com.android.launcher3.R +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** Screenshot tests for [TaskThumbnailView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun taskThumbnailView_uninitializedByDefault() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity) + } + } + + @Test + fun taskThumbnailView_resetsToUninitialized() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + val taskThumbnailView = createTaskThumbnailView(activity) + taskThumbnailView.setState(Uninitialized) + taskThumbnailView + } + } + + @Test + fun taskThumbnailView_recyclesToUninitialized() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + val taskThumbnailView = createTaskThumbnailView(activity) + taskThumbnailView.setState(BackgroundOnly(Color.YELLOW)) + taskThumbnailView.onRecycle() + taskThumbnailView + } + } + + @Test + fun taskThumbnailView_backgroundOnly() { + screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { setState(BackgroundOnly(Color.YELLOW)) } + } + } + + @Test + fun taskThumbnailView_liveTile_withoutHeader() { + screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(TaskThumbnailUiState.LiveTile.WithoutHeader) + } + } + } + + @Test + fun taskThumbnailView_image_withoutHeader() { + screenshotRule.screenshotTest("taskThumbnailView_image") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState( + SnapshotSplash( + Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY), + null, + ) + ) + } + } + } + + @Test + fun taskThumbnailView_image_withoutHeader_withImageMatrix() { + screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200 + setState( + SnapshotSplash( + Snapshot.WithoutHeader( + createBitmap( + width = VIEW_ENV_WIDTH / 2, + height = lessThanHeightMatchingAspectRatio, + ), + ROTATION_0, + Color.DKGRAY, + ), + null, + ) + ) + setImageMatrix(Matrix().apply { postScale(2f, 2f) }) + } + } + } + + @Test + fun taskThumbnailView_splash_withoutHeader() { + screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState( + SnapshotSplash( + Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY), + BitmapDrawable(activity.resources, createSplash()), + ) + ) + updateSplashAlpha(0.5f) + } + } + } + + @Test + fun taskThumbnailView_splash_withoutHeader_withImageMatrix() { + screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200 + setState( + SnapshotSplash( + Snapshot.WithoutHeader( + createBitmap( + width = VIEW_ENV_WIDTH / 2, + height = lessThanHeightMatchingAspectRatio, + ), + ROTATION_0, + Color.DKGRAY, + ), + BitmapDrawable(activity.resources, createSplash()), + ) + ) + setImageMatrix(Matrix().apply { postScale(2f, 2f) }) + updateSplashAlpha(0.5f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_tintAmount() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateTintAmount(.4f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_menuOpen() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateMenuOpenProgress(1f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_tintAmountAndMenuOpen() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_80") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateTintAmount(.8f) + updateMenuOpenProgress(1f) + } + } + } + + @Test + fun taskThumbnailView_scaled_roundRoundedCorners() { + screenshotRule.screenshotTest("taskThumbnailView_scaledRoundedCorners") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + scaleX = 0.75f + scaleY = 0.3f + setState(BackgroundOnly(Color.YELLOW)) + } + } + } + + private fun createTaskThumbnailView(context: Context): TaskThumbnailView { + val taskThumbnailView = + LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false) + as TaskThumbnailView + taskThumbnailView.cornerRadius = CORNER_RADIUS + return taskThumbnailView + } + + private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1) + + private fun createBitmap( + width: Int = VIEW_ENV_WIDTH, + height: Int = VIEW_ENV_HEIGHT, + rectColorRotation: Int = 0, + ) = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { + Canvas(this).apply { + val paint = Paint() + paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4] + drawRect(0f, 0f, width / 2f, height / 2f, paint) + paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4] + drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint) + paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4] + drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint) + paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4] + drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint) + } + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + + const val CORNER_RADIUS = 56f + val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN) + const val VIEW_ENV_WIDTH = 1440 + const val VIEW_ENV_HEIGHT = 3120 + } +} diff --git a/quickstep/tests/multivalentTests/multivalentTests1.iml b/quickstep/tests/multivalentTests/multivalentTests1.iml deleted file mode 100644 index aba0d60d2..000000000 --- a/quickstep/tests/multivalentTests/multivalentTests1.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/tests/multivalentTests/multivalentTestsForDevice.iml b/quickstep/tests/multivalentTests/multivalentTestsForDevice.iml deleted file mode 100644 index aba0d60d2..000000000 --- a/quickstep/tests/multivalentTests/multivalentTestsForDevice.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt new file mode 100644 index 000000000..7ebef45d2 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt @@ -0,0 +1,96 @@ +/* + * 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.launcher3.desktop + +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionFilter +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.SystemUiProxy +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopAppLaunchTransitionManagerTest { + + @get:Rule val mSetFlagsRule = SetFlagsRule() + + private val context = mock() + private val systemUiProxy = mock() + private lateinit var transitionManager: DesktopAppLaunchTransitionManager + + @Before + fun setUp() { + whenever(context.resources).thenReturn(mock()) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + fun registerTransitions_appLaunchFlagEnabled_registersTransition() { + transitionManager.registerTransitions() + + verify(systemUiProxy, times(1)).registerRemoteTransition(any(), any()) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + fun registerTransitions_appLaunchFlagDisabled_doesntRegisterTransition() { + transitionManager.registerTransitions() + + verify(systemUiProxy, times(0)).registerRemoteTransition(any(), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + fun registerTransitions_usesCorrectFilter() { + transitionManager.registerTransitions() + val filterArgumentCaptor = argumentCaptor() + + verify(systemUiProxy, times(1)) + .registerRemoteTransition(any(), filterArgumentCaptor.capture()) + + assertThat(filterArgumentCaptor.lastValue).isNotNull() + assertThat(filterArgumentCaptor.lastValue.mTypeSet) + .isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(filterArgumentCaptor.lastValue.mRequirements).hasLength(1) + val launchRequirement = filterArgumentCaptor.lastValue.mRequirements!![0] + assertThat(launchRequirement.mModes).isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(launchRequirement.mActivityType).isEqualTo(ACTIVITY_TYPE_STANDARD) + assertThat(launchRequirement.mWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java index d4dd58040..91f9e53ea 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java @@ -35,10 +35,13 @@ import android.os.UserHandle; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.AllModulesForTest; +import com.android.launcher3.util.SandboxContext; import com.android.launcher3.util.UserIconInfo; import com.android.systemui.shared.system.SysUiStatsLog; @@ -51,6 +54,9 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; +import dagger.BindsInstance; +import dagger.Component; + @SmallTest @RunWith(AndroidJUnit4.class) public class AppEventProducerTest { @@ -72,7 +78,9 @@ public class AppEventProducerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = new SandboxContext(getApplicationContext()); - mContext.putObject(UserCache.INSTANCE, mUserCache); + mContext.initDaggerComponent( + DaggerAppEventProducerTest_TestComponent.builder().bindUserCache(mUserCache) + ); mAppEventProducer = new AppEventProducer(mContext, null); } @@ -129,4 +137,15 @@ public class AppEventProducerTest { .build()); return itemBuilder.build(); } + + @LauncherAppSingleton + @Component(modules = { AllModulesForTest.class }) + interface TestComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance + AppEventProducerTest.TestComponent.Builder bindUserCache(UserCache userCache); + @Override LauncherAppComponent build(); + } + } } diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt similarity index 73% rename from quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt index a5327628d..1c7af1466 100644 --- a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt @@ -1,10 +1,24 @@ +/* + * 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.launcher3.model import android.app.prediction.AppPredictor import android.app.prediction.AppTarget import android.app.prediction.AppTargetEvent import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS @@ -19,7 +33,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations /** Unit tests for [QuickstepModelDelegate]. */ @@ -39,12 +53,18 @@ class QuickstepModelDelegateTest { fun setUp() { MockitoAnnotations.initMocks(this) modelHelper = LauncherModelHelper() - underTest = QuickstepModelDelegate(modelHelper.sandboxContext) + underTest = + QuickstepModelDelegate( + modelHelper.sandboxContext, + modelHelper.sandboxContext.appComponent.idp, + modelHelper.sandboxContext.appComponent.packageManagerHelper, + "", /* dbFileName */ + ) underTest.mAllAppsState.predictor = allAppsPredictor underTest.mHotseatState.predictor = hotseatPredictor underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor - underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext) - underTest.mDataModel = BgDataModel() + underTest.mModel = modelHelper.model + underTest.mDataModel = BgDataModel(WidgetsModel(modelHelper.sandboxContext)) } @After @@ -57,25 +77,25 @@ class QuickstepModelDelegateTest { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION) verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent) - verifyZeroInteractions(hotseatPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(hotseatPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) } @Test fun onWidgetPrediction_notifyWidgetRecommendationPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION) - verifyZeroInteractions(allAppsPredictor) + verifyNoMoreInteractions(allAppsPredictor) verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent) - verifyZeroInteractions(hotseatPredictor) + verifyNoMoreInteractions(hotseatPredictor) } @Test fun onHotseatPrediction_notifyHotseatPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION) - verifyZeroInteractions(allAppsPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(allAppsPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent) } @@ -83,8 +103,8 @@ class QuickstepModelDelegateTest { fun onOtherClient_notifyHotseatPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS) - verifyZeroInteractions(allAppsPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(allAppsPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent) } diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt similarity index 64% rename from quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt index 5c7b4aba4..d44518903 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt @@ -16,6 +16,8 @@ package com.android.launcher3.model +import android.app.prediction.AppPredictionManager +import android.app.prediction.AppPredictor import android.app.prediction.AppTarget import android.app.prediction.AppTargetEvent import android.app.prediction.AppTargetId @@ -30,14 +32,21 @@ import com.android.launcher3.DeviceProfile import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState import com.android.launcher3.icons.IconCache +import com.android.launcher3.model.WidgetPredictionsRequester.LAUNCH_LOCATION import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter import com.android.launcher3.util.ActivityContextWrapper -import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.Executors +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.TestUtil import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddWidgetInfo import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.function.Predicate import junit.framework.Assert.assertNotNull import org.junit.Before @@ -45,6 +54,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class WidgetsPredictionsRequesterTest { @@ -62,15 +74,30 @@ class WidgetsPredictionsRequesterTest { private lateinit var widgetItem1b: WidgetItem private lateinit var widgetItem2: WidgetItem - private lateinit var allWidgets: Map> + private lateinit var allWidgets: Map @Mock private lateinit var iconCache: IconCache + @Mock private lateinit var apmMock: AppPredictionManager + + @Mock private lateinit var predictorMock: AppPredictor + @Before fun setUp() { MockitoAnnotations.initMocks(this) mUserHandle = myUserHandle() - context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + + whenever(apmMock.createAppPredictionSession(any())).thenReturn(predictorMock) + + context = + object : ActivityContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? { + if (name == "app_prediction") { + return apmMock + } + return super.getSystemService(name) + } + } testInvariantProfile = LauncherAppState.getIDP(context) deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context) @@ -93,9 +120,9 @@ class WidgetsPredictionsRequesterTest { allWidgets = mapOf( - PackageUserKey(APP_1_PACKAGE_NAME, mUserHandle) to - listOf(widgetItem1a, widgetItem1b), - PackageUserKey(APP_2_PACKAGE_NAME, mUserHandle) to listOf(widgetItem2), + ComponentKey(widgetItem1a.componentName, widgetItem1a.user) to widgetItem1a, + ComponentKey(widgetItem1b.componentName, widgetItem1b.user) to widgetItem1b, + ComponentKey(widgetItem2.componentName, widgetItem2.user) to widgetItem2, ) } @@ -103,7 +130,7 @@ class WidgetsPredictionsRequesterTest { fun buildBundleForPredictionSession_includesAddedAppWidgets() { val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info) - val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE) + val bundle = buildBundleForPredictionSession(existingWidgets) val addedWidgetsBundleExtra = bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java) @@ -113,21 +140,67 @@ class WidgetsPredictionsRequesterTest { buildExpectedAppTargetEvent( /*pkg=*/ APP_1_PACKAGE_NAME, /*providerClassName=*/ APP_1_PROVIDER_A_CLASS_NAME, - /*user=*/ mUserHandle + /*user=*/ mUserHandle, ), buildExpectedAppTargetEvent( /*pkg=*/ APP_1_PACKAGE_NAME, /*providerClassName=*/ APP_1_PROVIDER_B_CLASS_NAME, - /*user=*/ mUserHandle + /*user=*/ mUserHandle, ), buildExpectedAppTargetEvent( /*pkg=*/ APP_2_PACKAGE_NAME, /*providerClassName=*/ APP_2_PROVIDER_1_CLASS_NAME, - /*user=*/ mUserHandle - ) + /*user=*/ mUserHandle, + ), ) } + @Test + fun request_invokesCallbackWithPredictedItems() { + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + val underTest = WidgetPredictionsRequester(context, TEST_UI_SURFACE, allWidgets) + val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo) + val predictions = + listOf( + // (existing) already on surface + AppTarget( + AppTargetId(APP_1_PACKAGE_NAME), + APP_1_PACKAGE_NAME, + APP_1_PROVIDER_B_CLASS_NAME, + mUserHandle, + ), + // eligible + AppTarget( + AppTargetId(APP_2_PACKAGE_NAME), + APP_2_PACKAGE_NAME, + APP_2_PROVIDER_1_CLASS_NAME, + mUserHandle, + ), + ) + doAnswer { + underTest.onTargetsAvailable(predictions) + null + } + .whenever(predictorMock) + .requestPredictionUpdate() + val testCountDownLatch = CountDownLatch(1) + val listener = + WidgetPredictionsRequester.WidgetPredictionsListener { itemInfos -> + if (itemInfos.size == 1 && itemInfos[0] is PendingAddWidgetInfo) { + // only one item was eligible. + testCountDownLatch.countDown() + } else { + println("Unexpected prediction items found: ${itemInfos.size}") + } + } + + underTest.request(existingWidgets, listener) + TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {} + + assertThat(testCountDownLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue() + } + } + @Test fun filterPredictions_notOnUiSurfaceFilter_returnsOnlyEligiblePredictions() { val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) @@ -140,15 +213,15 @@ class WidgetsPredictionsRequesterTest { AppTargetId(APP_1_PACKAGE_NAME), APP_1_PACKAGE_NAME, APP_1_PROVIDER_B_CLASS_NAME, - mUserHandle + mUserHandle, ), // eligible AppTarget( AppTargetId(APP_2_PACKAGE_NAME), APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME, - mUserHandle - ) + mUserHandle, + ), ) // only 2 was eligible @@ -156,7 +229,7 @@ class WidgetsPredictionsRequesterTest { } @Test - fun filterPredictions_appPredictions_returnsWidgetFromPackage() { + fun filterPredictions_appPredictions_returnsEmptyList() { val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) val filter: Predicate = notOnUiSurfaceFilter(widgetsAlreadyOnSurface) @@ -166,28 +239,27 @@ class WidgetsPredictionsRequesterTest { AppTargetId(APP_1_PACKAGE_NAME), APP_1_PACKAGE_NAME, "$APP_1_PACKAGE_NAME.SomeActivity", - mUserHandle + mUserHandle, ), AppTarget( AppTargetId(APP_2_PACKAGE_NAME), APP_2_PACKAGE_NAME, "$APP_2_PACKAGE_NAME.SomeActivity2", - mUserHandle + mUserHandle, ), ) - assertThat(filterPredictions(predictions, allWidgets, filter)) - .containsExactly(widgetItem1a, widgetItem2) + assertThat(filterPredictions(predictions, allWidgets, filter)).isEmpty() } - private fun createWidgetItem( - providerInfo: AppWidgetProviderInfo, - ): WidgetItem { + private fun createWidgetItem(providerInfo: AppWidgetProviderInfo): WidgetItem { val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo) return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context) } companion object { + const val TEST_TIMEOUT = 3L + const val TEST_UI_SURFACE = "widgets_test" const val BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets" @@ -203,18 +275,18 @@ class WidgetsPredictionsRequesterTest { private fun buildExpectedAppTargetEvent( pkg: String, providerClassName: String, - userHandle: UserHandle + userHandle: UserHandle, ): AppTargetEvent { val appTarget = AppTarget.Builder( /*id=*/ AppTargetId("widget:$pkg"), /*packageName=*/ pkg, - /*user=*/ userHandle + /*user=*/ userHandle, ) .setClassName(providerClassName) .build() return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) - .setLaunchLocation(TEST_UI_SURFACE) + .setLaunchLocation(LAUNCH_LOCATION) .build() } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt new file mode 100644 index 000000000..42adfec82 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt @@ -0,0 +1,228 @@ +/* + * 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.launcher3.model.data + +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE +import com.android.launcher3.model.data.TaskViewItemInfo.Companion.createTaskViewAtom +import com.android.launcher3.pm.UserCache +import com.android.launcher3.util.AllModulesForTest +import com.android.launcher3.util.SandboxContext +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.launcher3.util.UserIconInfo +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import org.junit.After +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 + +/** Test for [TaskViewItemInfo] */ +@RunWith(AndroidJUnit4::class) +class TaskViewItemInfoTest { + private val context = SandboxContext(InstrumentationRegistry.getInstrumentation().targetContext) + private val taskView = mock() + private val recentsView = mock>() + private val overlayFactory = mock() + private val userCache = mock() + private val userInfo = mock() + + @Before + fun setUp() { + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + whenever(taskView.context).thenReturn(context) + whenever(taskView.recentsView).thenReturn(recentsView) + whenever(recentsView.indexOfChild(taskView)).thenReturn(TASK_VIEW_INDEX) + whenever(userInfo.isPrivate).thenReturn(false) + whenever(userCache.getUserInfo(any())).thenReturn(userInfo) + context.initDaggerComponent( + DaggerTaskViewItemInfoTest_TestComponent.builder().bindUserCache(userCache) + ) + RecentsDependencies.maybeInitialize(context) + } + + @After + fun tearDown() { + RecentsDependencies.destroy(context) + } + + @Test + fun singleTask() { + val taskContainers = listOf(createTaskContainer(createTask(1))) + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 0, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 1, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun splitTask() { + val taskContainers = + listOf(createTaskContainer(createTask(1)), createTaskContainer(createTask(2))) + whenever(taskView.type).thenReturn(TaskViewType.GROUPED) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 1, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 2, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun desktopTask() { + val taskContainers = + listOf( + createTaskContainer(createTask(1)), + createTaskContainer(createTask(2)), + createTaskContainer(createTask(3)), + ) + whenever(taskView.type).thenReturn(TaskViewType.DESKTOP) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 2, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 3, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun privateTask() { + val taskContainers = listOf(createTaskContainer(createTask(1))) + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.taskContainers).thenReturn(taskContainers) + whenever(userInfo.isPrivate).thenReturn(true) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 0, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 1, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE) + .isEqualTo(FLAG_NOT_PINNABLE) + } + + @Test + fun emptyDesktopTask() { + whenever(taskView.type).thenReturn(TaskViewType.DESKTOP) + + val taskViewItemInfo = TaskViewItemInfo(taskView = taskView, taskContainer = null) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 2, + index = TASK_VIEW_INDEX, + componentName = "", + cardinality = 0, + ) + ) + assertThat(taskViewItemInfo.user).isEqualTo(Process.myUserHandle()) + assertThat(taskViewItemInfo.intent).isNotNull() + } + + private fun createTask(id: Int) = + Task(TaskKey(id, 0, Intent(), ComponentName(PACKAGE, CLASS), 0, 2000)) + + private fun createTaskContainer(task: Task): TaskContainer { + return TaskContainer( + taskView, + task, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) + } + + @LauncherAppSingleton + @Component(modules = [AllModulesForTest::class]) + interface TestComponent : LauncherAppComponent { + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindUserCache(userCache: UserCache): Builder + + override fun build(): TestComponent + } + } + + companion object { + const val PACKAGE = "package" + const val CLASS = "class" + const val TASK_VIEW_INDEX = 4 + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt similarity index 97% rename from quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt index 04012c027..b39c3f1e3 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt @@ -17,7 +17,7 @@ package com.android.launcher3.taskbar -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.statemanager.StateManager import com.android.quickstep.RecentsActivity import com.android.quickstep.fallback.RecentsState @@ -33,7 +33,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() { - lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController + lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController lateinit var stateListener: StateManager.StateListener private val recentsActivity: RecentsActivity = mock() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt new file mode 100644 index 000000000..50d6affe4 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt @@ -0,0 +1,109 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.SystemUiProxy +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarAutohideSuspendControllerTest { + + @get:Rule(order = 0) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams({ + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> + doAnswer { latestSuspendNotification = it.getArgument(0) } + .whenever(proxy) + .notifyTaskbarAutohideSuspend(anyOrNull()) + } + }) + ) + @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var stashController: TaskbarStashController + + private var latestSuspendNotification: Boolean? = null + + @Test + fun testUpdateFlag_suspendInLauncher_notifiesSuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, true) + } + assertThat(latestSuspendNotification).isTrue() + } + + @Test + fun testUpdateFlag_toggleSuspendDraggingTwice_notifiesUnsuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, true) + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false) + } + assertThat(latestSuspendNotification).isFalse() + } + + @Test + fun testUpdateFlag_resetsAlreadyUnsetFlag_noNotifyUnsuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false) + } + assertThat(latestSuspendNotification).isNull() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateFlag_suspendTransientTaskbarForTouch_cancelsAutoStashTimeout() { + // Unstash and verify alarm. + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + // EDU opens while unstashed. + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_TOUCHING, true) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isFalse() + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt similarity index 94% rename from quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt index 15b1e532b..52d288a72 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController import com.android.launcher3.taskbar.bubbles.BubbleControllers +import com.android.launcher3.taskbar.growth.NudgeController import com.android.launcher3.taskbar.overlay.TaskbarOverlayController import com.android.systemui.shared.rotation.RotationButtonController import java.util.Optional @@ -57,6 +58,8 @@ abstract class TaskbarBaseTestCase { @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController @Mock lateinit var taskbarPinningController: TaskbarPinningController @Mock lateinit var optionalBubbleControllers: Optional + @Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController + @Mock lateinit var nudgeController: NudgeController lateinit var taskbarControllers: TaskbarControllers @@ -98,6 +101,8 @@ abstract class TaskbarBaseTestCase { keyboardQuickSwitchController, taskbarPinningController, optionalBubbleControllers, + taskbarDesktopModeController, + nudgeController, ) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt new file mode 100644 index 000000000..0e066cd13 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt @@ -0,0 +1,50 @@ +/* + * 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.launcher3.taskbar + +import android.content.Context +import com.android.launcher3.ConstantItem +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.TestUtil +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +object TaskbarControllerTestUtil { + inline fun runOnMainSync(crossinline runTest: () -> Unit) { + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) { runTest() } + } + + /** Returns a property to read/write the value of a [ConstantItem]. */ + fun ConstantItem.asProperty(context: Context): ReadWriteProperty { + return TaskbarItemProperty(context, this) + } + + private class TaskbarItemProperty( + private val context: Context, + private val item: ConstantItem, + ) : ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return LauncherPrefs.get(context).get(item) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + runOnMainSync { LauncherPrefs.get(context).put(item, value) } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt new file mode 100644 index 000000000..455b6c5b9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt @@ -0,0 +1,48 @@ +/* + * 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.launcher3.taskbar + +import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarDesktopModeControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @TaskbarUnitTestRule.InjectController + lateinit var taskbarDesktopModeController: TaskbarDesktopModeController + + @Test + fun whenTaskbarRequiresCornerRoundness_shouldReturnDefaultCornerRoundness() { + assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(true)) + .isEqualTo(MAX_ROUNDNESS) + } + + @Test + fun whenTaskbarRequiresCornerRoundness_shouldReturnZeroAsCornerRoundness() { + assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(false)).isEqualTo(0f) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt new file mode 100644 index 000000000..3c8035232 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt @@ -0,0 +1,187 @@ +/* + * 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.launcher3.taskbar + +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.OnboardingPrefs +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarEduTooltipControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private val wasInTestHarness = Utilities.isRunningInTestHarness() + + private var tooltipStep by OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem.asProperty(context) + private var searchEduSeen by OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN.asProperty(context) + + @Before + fun setUp() { + Utilities.disableRunningInTestHarnessForTests() + } + + @After + fun tearDown() { + if (wasInTestHarness) { + Utilities.enableRunningInTestHarnessForTests() + } + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_FEATURES + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() { + tooltipStep = TOOLTIP_STEP_NONE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() { + tooltipStep = TOOLTIP_STEP_FEATURES + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() { + tooltipStep = TOOLTIP_STEP_PINNING + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() { + // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu. + tooltipStep = TOOLTIP_STEP_PINNING + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() { + tooltipStep = TOOLTIP_STEP_NONE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHide_whenTooltipIsOpen_shouldCloseTooltip() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + runOnMainSync { taskbarEduTooltipController.hide() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSearchEdu_whenTaskbarIsTransient_shouldNotShowSearchEdu() { + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() { + searchEduSeen = true + assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue() + runOnMainSync { taskbarEduTooltipController.hide() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt new file mode 100644 index 000000000..d2b9fcf2c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.BubbleTextView +import com.android.launcher3.R +import com.android.launcher3.apppairs.AppPairIcon +import com.android.launcher3.folder.FolderIcon +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatAppPairsItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatFolderItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarHoverToolTipControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var popupController: TaskbarPopupController + + private val taskbarContext: TaskbarActivityContext by taskbarUnitTestRule::activityContext + + private lateinit var taskbarView: TaskbarView + private lateinit var iconView: BubbleTextView + private lateinit var appPairIcon: AppPairIcon + private lateinit var folderIcon: FolderIcon + + private val isHoverToolTipOpen: Boolean + get() { + // TaskbarHoverToolTip uses ArrowTipView which is type TYPE_ON_BOARD_POPUP. + return AbstractFloatingView.hasOpenView( + taskbarContext, + AbstractFloatingView.TYPE_ON_BOARD_POPUP, + ) + } + + @Before + fun setup() { + runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) } + + val hotseatItems = + arrayOf( + createHotseatWorkspaceItem(), + createHotseatAppPairsItem(), + createHotseatFolderItem(), + ) + runOnMainSync { + taskbarView.updateItems(hotseatItems, emptyList()) + iconView = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + appPairIcon = taskbarView.iconViews.filterIsInstance().first() + folderIcon = taskbarView.iconViews.filterIsInstance().first() + } + } + + @Test + fun onHover_hoverEnterIcon_revealToolTip_hoverExitIcon_closeToolTip() { + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterFolderIcon_revealToolTip_hoverExitFolderIcon_closeToolTip() { + runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterAppPair_revealToolTip_hoverExitAppPair_closeToolTip() { + runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterIconAlignedWithHotseat_noToolTip() { + taskbarContext.setUIController( + object : TaskbarUIController() { + override fun isIconAlignedWithHotseat(): Boolean = true + } + ) + + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isFalse() + } + + @Test + fun onHover_hoverEnterFolderOpen_noToolTip() { + runOnMainSync { + folderIcon.folder.animateOpen() + iconView.dispatchGenericMotionEvent(HOVER_ENTER) + } + assertThat(isHoverToolTipOpen).isFalse() + } + + @Test + fun onHover_hoverEnterPopupOpen_noToolTip() { + runOnMainSync { + popupController.showForIcon(iconView) + iconView.dispatchGenericMotionEvent(HOVER_ENTER) + } + assertThat(isHoverToolTipOpen).isFalse() + } + + companion object { + private val HOVER_EXIT = MotionEvent.obtain(0, 0, ACTION_HOVER_EXIT, 0f, 0f, 0) + private val HOVER_ENTER = MotionEvent.obtain(0, 0, ACTION_HOVER_ENTER, 0f, 0f, 0) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt similarity index 97% rename from quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt index e619e7cd3..24310202c 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar import android.app.KeyguardManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING @@ -23,6 +24,7 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_B import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED 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.never @@ -30,6 +32,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@RunWith(AndroidJUnit4::class) class TaskbarKeyguardControllerTest : TaskbarBaseTestCase() { private val baseDragLayer: TaskbarDragLayer = mock() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 0f06d9874..a8f3500a7 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -4,6 +4,8 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; @@ -13,11 +15,14 @@ import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IM import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; +import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -25,7 +30,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.Flags; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -34,11 +44,14 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.systemui.contextualeducation.GestureType; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -49,12 +62,13 @@ public class TaskbarNavButtonControllerTest { @Mock SystemUiProxy mockSystemUiProxy; + @Mock TouchInteractionService mockService; @Mock Handler mockHandler; @Mock - AssistUtils mockAssistUtils; + ContextualSearchInvoker mockContextualSearchInvoker; @Mock StatsLogManager mockStatsLogManager; @Mock @@ -66,6 +80,9 @@ public class TaskbarNavButtonControllerTest { @Mock View mockView; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private int mHomePressCount; private int mOverviewToggleCount; private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() { @@ -98,19 +115,45 @@ public class TaskbarNavButtonControllerTest { mCallbacks, mockSystemUiProxy, mockHandler, - mockAssistUtils); + mockContextualSearchInvoker); } @Test public void testPressBack() { mNavButtonController.onButtonClick(BUTTON_BACK, mockView); - verify(mockSystemUiProxy, times(1)).onBackPressed(); + verify(mockSystemUiProxy, times(1)).onBackEvent(null); + } + + @Test + public void testPressBack_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_BACK, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.BACK)); } @Test public void testPressImeSwitcher() { + mNavButtonController.init(mockTaskbarControllers); mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed(); + verify(mockSystemUiProxy, never()).onImeSwitcherLongPress(); + } + + @Test + public void testLongPressImeSwitcher() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonLongClick(BUTTON_IME_SWITCH, mockView); + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); + verify(mockSystemUiProxy, never()).onImeSwitcherPressed(); + if (Flags.imeSwitcherRevamp()) { + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mockSystemUiProxy, times(1)).onImeSwitcherLongPress(); + } else { + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mockSystemUiProxy, never()).onImeSwitcherLongPress(); + } } @Test @@ -129,40 +172,40 @@ public class TaskbarNavButtonControllerTest { @Test public void testLongPressHome_enabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, times(1)).startAssistant(any()); } @Test public void testLongPressHome_enabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @@ -172,12 +215,26 @@ public class TaskbarNavButtonControllerTest { assertThat(mHomePressCount).isEqualTo(1); } + @Test + public void testPressHome_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.HOME)); + } + @Test public void testPressRecents() { mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); assertThat(mOverviewToggleCount).isEqualTo(1); } + @Test + public void testPressRecents_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.OVERVIEW)); + } + @Test public void testPressRecentsWithScreenPinned_noNavigationToOverview() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); @@ -282,4 +339,46 @@ public class TaskbarNavButtonControllerTest { verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testPredictiveBackInvoked() { + ArgumentCaptor keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false); + verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture()); + verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false); + verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, false); + } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testPredictiveBackCancelled() { + ArgumentCaptor keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true); + verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture()); + verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false); + verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, true); + } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testButtonsDisabledWhileBackPressed() { + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); + mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView); + mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false); + assertThat(mHomePressCount).isEqualTo(0); + verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked(); + assertThat(mOverviewToggleCount).isEqualTo(0); + verify(mockSystemUiProxy, never()).onImeSwitcherPressed(); + } + + private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) { + assertEquals(isCancelled, keyEvent.isCanceled()); + assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction()); + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt new file mode 100644 index 000000000..c58941519 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt @@ -0,0 +1,562 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR +import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW +import com.android.launcher3.R +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.DisplayControllerModule +import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper +import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.AllModulesForTest +import com.android.launcher3.util.FakePrefsModule +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.TestUtil.getOnUiThread +import com.android.quickstep.RecentsModel +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesktopTask +import com.android.systemui.shared.recents.model.Task +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.android.wm.shell.desktopmode.IDesktopTaskListener +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +@EnableFlags( + FLAG_TASKBAR_OVERFLOW, + FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_BUBBLE_BAR, +) +@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) +class TaskbarOverflowTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper() + + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams( + { + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> + doAnswer { desktopTaskListener = it.getArgument(0) } + .whenever(proxy) + .setDesktopTaskListener(anyOrNull()) + } + }, + DaggerTaskbarOverflowComponent.builder() + .bindRecentsModel(mockRecentsModelHelper.mockRecentsModel), + ) + ) + + @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper) + + @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + + @get:Rule(order = 5) + val taskbarUnitTestRule = TaskbarUnitTestRule(this, context, this::onControllersInitialized) + + @InjectController lateinit var taskbarViewController: TaskbarViewController + @InjectController lateinit var recentAppsController: TaskbarRecentAppsController + @InjectController lateinit var bubbleBarViewController: BubbleBarViewController + @InjectController lateinit var bubbleStashController: BubbleStashController + @InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController + + private var desktopTaskListener: IDesktopTaskListener? = null + + private var currentControllerInitCallback: () -> Unit = {} + set(value) { + runOnMainSync { value.invoke() } + field = value + } + + private fun onControllersInitialized() { + runOnMainSync { + if (!recentAppsController.canShowRunningApps) { + recentAppsController.onDestroy() + recentAppsController.canShowRunningApps = true + recentAppsController.init(taskbarUnitTestRule.activityContext.controllers) + } + + currentControllerInitCallback.invoke() + } + } + + @Before + fun ensureRunningAppsShowing() { + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarWithMaxNumIcons_pinned() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarWithMaxNumIcons_transient() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbar_pinned() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOverflownTaskbar_transient() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbarWithNoSpaceForRecentApps_pinned() { + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + // Create two "recent" desktop tasks, and then add enough hotseat items so the taskbar + // reaches max number of items with hotseat item icons, all apps and divider icons only. + // I.e. so all desktop tasks are in taskbar overflow. + createDesktopTask(2) + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount), + recentAppsController.shownTasks, + ) + } + + // Verify that taskbar overflow view is shown (eventhough it exceeds max taskbar icons). + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) + assertThat(taskbarOverflowIconIndex).isEqualTo(maxNumberOfTaskbarIcons) + assertThat(overflowItems).containsExactlyElementsIn(0..1) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbarWithNoSpaceForRecentApps_singleRecent_pinned() { + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + // Create a "recent" desktop task, and then add enough hotseat items so the taskbar + // reaches max number of items with hotseat item icons, all apps and divider icons only. + // I.e. so the single desktop tasks is in taskbar overflow. + createDesktopTask(1) + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + val hotseatItems = createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount) + + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + // Verify that recent task is shown (eventhough it exceeds max taskbar icons), and that + // the taskbar overflow view is not added for the single recent app. + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) + assertThat(taskbarOverflowIconIndex).isEqualTo(-1) + } + + @Test + @TaskbarMode(PINNED) + fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transient() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + currentControllerInitCallback = { + bubbleStashController.stashBubbleBarImmediate() + bubbleBarViewController.setHiddenForBubbles(false) + } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testStashingBubbleBarMaintainsMaxNumIcons_transient() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + + runOnMainSync { bubbleStashController.stashBubbleBarImmediate() } + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + } + + @Test + @TaskbarMode(PINNED) + fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } + runOnMainSync { animatorTestRule.advanceTimeBy(150) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHidingBubbleBarIncreasesMaxNumIcons_transient() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } + runOnMainSync { animatorTestRule.advanceTimeBy(150) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testPressingOverflowButtonOpensKeyboardQuickSwitch() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createDesktopTask(createdTasks) + + assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount) + tapOverflowIcon() + // Keyboard quick switch view is shown only after list of recent task is asynchronously + // retrieved from the recents model. + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + + assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() + assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) + .containsExactlyElementsIn(0..), + recentAppsController.shownTasks, + ) + } + + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount + hotseatItems.size) + assertThat(overflowItems) + .containsExactlyElementsIn(listOf(0) + (2..targetOverflowSize + 1).toList()) + } + + @Test + @TaskbarMode(PINNED) + fun testHotseatItemTasksNotShownInKQS() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + val hotseatItems = createHotseatItems(1) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createDesktopTaskWithTasksFromPackages( + listOf("fake") + + listOf(hotseatItems[0]?.targetPackage ?: "") + + List(createdTasks - 2) { "fake" } + ) + + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + tapOverflowIcon() + // Keyboard quick switch view is shown only after list of recent task is asynchronously + // retrieved from the recents model. + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + + assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() + assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) + .containsExactlyElementsIn(listOf(0) + (2..) { + val tasks = + packages.mapIndexed({ index, p -> + Task( + Task.TaskKey( + index, + 0, + Intent().apply { `package` = p }, + ComponentName(p, ""), + Process.myUserHandle().identifier, + 2000, + ) + ) + }) + + recentsModel.updateRecentTasks(listOf(DesktopTask(deskId = 0, DEFAULT_DISPLAY, tasks))) + for (task in 1..tasks.size) { + desktopTaskListener?.onTasksVisibilityChanged( + context.virtualDisplay.display.displayId, + task, + ) + } + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + } + + private val navButtonEndSpacing: Int + get() { + return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize( + taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing + ) + } + + private val taskbarOverflowIconIndex: Int + get() { + return getOnUiThread { + taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView } + } + } + + private val maxNumberOfTaskbarIcons: Int + get() = getOnUiThread { taskbarViewController.maxNumIconViews } + + private val currentNumberOfTaskbarIcons: Int + get() = getOnUiThread { taskbarViewController.iconViews.size } + + private val taskbarIconsCentered: Boolean + get() { + return getOnUiThread { + val iconLayoutBounds = + taskbarViewController.transientTaskbarIconLayoutBoundsInParent + val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx + iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2 + } + } + + private val taskbarEndMargin: Int + get() { + return getOnUiThread { + taskbarUnitTestRule.activityContext.deviceProfile.widthPx - + taskbarViewController.transientTaskbarIconLayoutBoundsInParent.right + } + } + + private val overflowItems: List + get() { + return getOnUiThread { + val overflowIcon = + taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } + + if (overflowIcon is TaskbarOverflowView) { + overflowIcon.itemIds + } else { + emptyList() + } + } + } + + private fun tapOverflowIcon() { + runOnMainSync { + val overflowIcon = + taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } + assertThat(overflowIcon?.callOnClick()).isTrue() + } + } + + /** + * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies + * * max number of icons in the taskbar remains unchanged + * * number of icons in the taskbar is at most max number of icons + * * whether the taskbar overflow icon is shown, and its position in taskbar. + * + * Returns max number of icons. + */ + private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int { + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(0) + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + assertThat(initialIconCount).isLessThan(maxNumIconViews) + + createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize) + + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex) + .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1) + if (targetOverflowSize > 0) { + assertThat(overflowItems).containsExactlyElementsIn(0..targetOverflowSize) + } + return maxNumIconViews + } +} + +/** TaskbarOverflowComponent used to bind the RecentsModel. */ +@LauncherAppSingleton +@Component( + modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class] +) +interface TaskbarOverflowComponent : TaskbarSandboxComponent { + + @Component.Builder + interface Builder : TaskbarSandboxComponent.Builder { + @BindsInstance fun bindRecentsModel(model: RecentsModel): Builder + + override fun build(): TaskbarOverflowComponent + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt new file mode 100644 index 000000000..6bb320500 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.platform.test.annotations.DisableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.BubbleTextView +import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR +import com.android.launcher3.Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU +import com.android.launcher3.R +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.util.GroupTask +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR, FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) +class TaskbarPopupControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var popupController: TaskbarPopupController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private lateinit var taskbarView: TaskbarView + private lateinit var hotseatIcon: BubbleTextView + private lateinit var recentTaskIcon: BubbleTextView + + @Before + fun setup() { + taskbarContext.controllers.uiController.init(taskbarContext.controllers) + runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) } + + val hotseatItems = arrayOf(createHotseatWorkspaceItem()) + val recentItems = createRecents(2) + runOnMainSync { + taskbarView.updateItems(hotseatItems, recentItems) + hotseatIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + recentTaskIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is GroupTask + } + } + } + + @Test + fun showForIcon_hotseatItem() { + assertThat(hasPopupMenu()).isFalse() + runOnMainSync { popupController.showForIcon(hotseatIcon) } + assertThat(hasPopupMenu()).isTrue() + } + + @Test + fun showForIcon_recentTask() { + assertThat(hasPopupMenu()).isFalse() + runOnMainSync { popupController.showForIcon(recentTaskIcon) } + assertThat(hasPopupMenu()).isTrue() + } + + private fun hasPopupMenu(): Boolean { + return AbstractFloatingView.hasOpenView( + taskbarContext, + AbstractFloatingView.TYPE_ACTION_POPUP, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt new file mode 100644 index 000000000..334d8ab06 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -0,0 +1,994 @@ +/* + * 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.launcher3.taskbar + +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.graphics.Rect +import android.os.Process +import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.annotation.UiThreadTest +import com.android.internal.R +import com.android.launcher3.BubbleTextView.RunningAppState +import com.android.launcher3.Flags +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarRecentAppsController.TaskState +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.TaskIconCache +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitTask +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) +@EnableFlags(Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) +class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { + + @get:Rule val mockitoRule = MockitoJUnit.rule() + @get:Rule + val disableControllerForCertainTestsWatcher = + object : TestWatcher() { + override fun starting(description: Description) { + // Update canShowRunningAndRecentAppsAtInit before setUp() is called for each test. + canShowRunningAndRecentAppsAtInit = + description.methodName !in + listOf("canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled") + } + } + + @Mock private lateinit var mockIconCache: TaskIconCache + @Mock private lateinit var mockRecentsModel: RecentsModel + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + + private var taskListChangeId: Int = 1 + + private lateinit var recentAppsController: TaskbarRecentAppsController + private lateinit var myUserHandle: UserHandle + private val USER_HANDLE_1 = UserHandle.of(1) + private val USER_HANDLE_2 = UserHandle.of(2) + + private var canShowRunningAndRecentAppsAtInit = true + private var recentTasksChangedListener: RecentTasksChangedListener? = null + + val recentShownTasks: List + get() = recentAppsController.shownTasks.flatMap { it.tasks } + + @Before + fun setUp() { + super.setup() + myUserHandle = Process.myUserHandle() + + // Set desktop mode supported + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(mockResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true) + + whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache) + whenever(mockRecentsModel.unregisterRecentTasksChangedListener()).then { + recentTasksChangedListener = null + it + } + recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel) + recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit + recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit + recentAppsController.init(taskbarControllers) + taskbarControllers.onPostInit() + + recentTasksChangedListener = + if (canShowRunningAndRecentAppsAtInit) { + val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java) + verify(mockRecentsModel) + .registerRecentTasksChangedListener(listenerCaptor.capture()) + listenerCaptor.value + } else { + verify(mockRecentsModel, never()).registerRecentTasksChangedListener(any()) + null + } + + // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI() + whenever(taskbarViewController.commitRunningAppsToUI()).then { + recentAppsController.updateHotseatItemInfos( + recentAppsController.shownHotseatItems.toTypedArray() + ) + } + } + + // See the TestWatcher rule at the top which sets canShowRunningAndRecentAppsAtInit = false. + @Test + fun canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(mockRecentsModel, never()).getTasks(any>>()) + } + + @Test + fun canShowRunningAndRecentAppsIsFalseAfterInit_getTasksOnlyCalledInInit() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any>>(), any()) + recentAppsController.canShowRunningApps = false + recentAppsController.canShowRecentApps = false + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + // Verify that getTasks() was not called again after the init(). + verify(mockRecentsModel, times(1)).getTasks(any>>(), any()) + } + + @Test + fun getDesktopItemState_nullItemInfo_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(/* itemInfo= */ null) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_noItemPackage_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(ItemInfo()) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_noMatchingTasks_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(createItemInfo("package")) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_matchingVisibleTask_returnsVisible() { + setInDesktopMode(true) + val visibleTask = createTask(id = 1, "visiblePackage", isVisible = true) + updateRecentTasks(runningTasks = listOf(visibleTask), recentTaskPackages = emptyList()) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("visiblePackage")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.RUNNING, taskId = 1)) + } + + @Test + fun getDesktopItemState_matchingMinimizedTask_returnsMinimized() { + setInDesktopMode(true) + val minimizedTask = createTask(id = 1, "minimizedPackage", isVisible = false) + updateRecentTasks(runningTasks = listOf(minimizedTask), recentTaskPackages = emptyList()) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("minimizedPackage")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.MINIMIZED, taskId = 1)) + } + + @Test + fun getDesktopItemState_matchingMinimizedAndRunningTask_returnsVisible() { + setInDesktopMode(true) + updateRecentTasks( + runningTasks = + listOf( + createTask(id = 1, "package", isVisible = false), + createTask(id = 2, "package", isVisible = true), + ), + recentTaskPackages = emptyList(), + ) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("package")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.RUNNING, taskId = 2)) + } + + @Test + fun getDesktopItemState_noMatchingUserId_returnsNotRunning() { + setInDesktopMode(true) + updateRecentTasks( + runningTasks = + listOf( + createTask(id = 1, "package", isVisible = false, USER_HANDLE_1), + createTask(id = 2, "package", isVisible = true, USER_HANDLE_1), + ), + recentTaskPackages = emptyList(), + ) + + val taskState = + recentAppsController.getDesktopItemState(createItemInfo("package", USER_HANDLE_2)) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getRunningAppState_taskNotRunningOrMinimized_returnsNotRunning() { + setInDesktopMode(true) + updateRecentTasks(runningTasks = emptyList(), recentTaskPackages = emptyList()) + + assertThat(recentAppsController.getRunningAppState(taskId = 1)) + .isEqualTo(RunningAppState.NOT_RUNNING) + } + + @Test + fun getRunningAppState_taskNotVisible_returnsMinimized() { + setInDesktopMode(true) + val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false) + val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true) + updateRecentTasks(runningTasks = listOf(task1, task2), recentTaskPackages = emptyList()) + + assertThat(recentAppsController.getRunningAppState(taskId = 1)) + .isEqualTo(RunningAppState.MINIMIZED) + } + + @Test + fun getRunningAppState_taskVisible_returnsRunning() { + setInDesktopMode(true) + val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false) + val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true) + updateRecentTasks(runningTasks = listOf(task1, task2), recentTaskPackages = emptyList()) + + assertThat(recentAppsController.getRunningAppState(taskId = 2)) + .isEqualTo(RunningAppState.RUNNING) + } + + @Test + fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) + } + + @Test + fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRecentApps = false + setInDesktopMode(false) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) + } + + @Test + fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() { + recentAppsController.canShowRunningApps = true + setInDesktopMode(true) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) + } + + @Test + fun updateHotseatItemInfos_inDesktopMode_hotseatPackageHasRunningTask_hotseatItemLinksToTask() { + setInDesktopMode(true) + + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)), + recentTaskPackages = emptyList(), + ) + + assertThat(newHotseatItems).hasLength(2) + assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java) + assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java) + val hotseatItem1 = newHotseatItems[0] as TaskItemInfo + assertThat(hotseatItem1.taskId).isEqualTo(1) + } + + /** + * Tests that in desktop mode, when two tasks have the same package name and one is in the + * hotseat, only the hotseat item represents the app, and no duplicate is shown in recent apps. + */ + @Test + fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_onlyHotseatCoversTask() { + setInDesktopMode(true) + + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = + listOf( + createTask(id = 1, HOTSEAT_PACKAGE_1), + createTask(id = 2, HOTSEAT_PACKAGE_1), + ), + recentTaskPackages = emptyList(), + ) + + // The task is in Hotseat Items + assertThat(newHotseatItems).hasLength(2) + assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java) + assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java) + val hotseatItem1 = newHotseatItems[0] as TaskItemInfo + assertThat(hotseatItem1.targetPackage).isEqualTo(HOTSEAT_PACKAGE_1) + + // The other task of the same package is not in recentShownTasks + assertThat(recentShownTasks).isEmpty() + } + + @Test + fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() { + recentAppsController.canShowRecentApps = true + setInDesktopMode(false) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = + listOf( + createTask(id = 1, RUNNING_APP_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_2), + ), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_cantShowRecent_notInDesktopMode_shownTasks_returnsEmptyList() { + recentAppsController.canShowRecentApps = false + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_noRecentTasks_shownTasks_returnsEmptyList() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = + listOf( + createTask(id = 1, RUNNING_APP_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_2), + ), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() { + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks).containsExactlyElementsIn(listOf(task1, task2)) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() { + setInDesktopMode(false) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).isEmpty() + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() { + setInDesktopMode(true) + val runningTasks = + listOf( + createTask(id = 1, HOTSEAT_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_1), + createTask(id = 3, RUNNING_APP_PACKAGE_2), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = runningTasks, + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3)) + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_allAppsRunningAndInvisibleAppsMinimized() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3Minimized = createTask(id = 3, RUNNING_APP_PACKAGE_3, isVisible = false) + val runningTasks = listOf(task1, task2, task3Minimized) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3) + assertThat(recentAppsController.minimizedTaskIds).containsExactly(3) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_samePackage_differentTasks_severalRunningTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + + assertThat(recentShownTasks).isEqualTo(listOf(task1, task2)) + } + + /** + * Tests that when multiple instances of the same app are running in desktop mode and the app is + * not in the hotseat, only one instance is shown in the recent apps section. + */ + @Test + fun onRecentTasksChanged_inDesktopMode_multiInstance_noHotseat_shownTasksHasOneInstance() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + + // Assert that recentShownTasks contains only one instance of the app + assertThat(recentShownTasks).hasSize(1) + assertThat(recentShownTasks[0].key.packageName).isEqualTo(RUNNING_APP_PACKAGE_1) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_1). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1, task3), + recentTaskPackages = emptyList(), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + val expectedOrder = + listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) + assertThat(shownPackages).isEqualTo(expectedOrder) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_addTask_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_1). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2, task3), + recentTaskPackages = emptyList(), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_removeTask_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_3). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2)) + } + + @Test + fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() { + setInDesktopMode(false) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = recentTaskPackages, + ) + + setInDesktopMode(true) + recentTasksChangedListener!!.onRecentTasksChanged() + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + } + + @Test + fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() { + setInDesktopMode(true) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = recentTaskPackages, + ) + setInDesktopMode(false) + recentTasksChangedListener!!.onRecentTasksChanged() + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Don't expect RECENT_PACKAGE_3 because it is currently running. + val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentTasks_shownTasks_returnsRecentTasks() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded. + val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() { + setInDesktopMode(false) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + val shownPackages = recentAppsController.shownTasks.map { it.packageNames } + // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task + val desktopTilePackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1) + val expectedPackages = listOf(desktopTilePackages, recentTaskPackages) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentAndSplitTasks_shownTasks_returnsRecentTaskAndPair() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + val shownPackages = recentAppsController.shownTasks.map { it.packageNames } + // Only 2 recent tasks shown: Pair + 1 Recent Task + val pairPackages = RECENT_SPLIT_PACKAGES_1.split("_") + val recentTaskPackages = listOf(RECENT_PACKAGE_1) + val expectedPackages = listOf(pairPackages, recentTaskPackages) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_noActualChangeToRecents_commitRunningAppsToUI_notCalled() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() { + setInDesktopMode(true) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() { + setInDesktopMode(true) + val task1Minimized = createTask(id = 1, RUNNING_APP_PACKAGE_1, isVisible = false) + val task2Visible = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task2Minimized = createTask(id = 2, RUNNING_APP_PACKAGE_2, isVisible = false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1Minimized, task2Visible), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + + // Call onRecentTasksChanged() again with a new minimized app, verify we update UI. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1Minimized, task2Minimized), + recentTaskPackages = emptyList(), + ) + + verify(taskbarViewController, times(2)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() { + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + val originalTasks = listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1)) + val newTasks = + listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1), createTask(id = 2, HOTSEAT_PACKAGE_1)) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = originalTasks, + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + + // Call onRecentTasksChanged() again with a new running app, verify we update UI. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = newTasks, + recentTaskPackages = emptyList(), + ) + + verify(taskbarViewController, times(2)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_sameHotseatPackage_differentUser_isInShownTasks() { + setInDesktopMode(true) + val hotseatPackageUser = PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_2) + val hotseatPackageUsers = listOf(hotseatPackageUser) + val runningTask = createTask(id = 1, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val runningTasks = listOf(runningTask) + prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers = hotseatPackageUsers, + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks).contains(runningTask) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1)) + } + + private fun prepareHotseatAndRunningAndRecentApps( + hotseatPackages: List, + runningTasks: List, + recentTaskPackages: List, + ): Array { + val hotseatPackageUsers = hotseatPackages.map { PackageUser(it, myUserHandle) } + return prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers, + runningTasks, + recentTaskPackages, + ) + } + + private fun prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers: List, + runningTasks: List, + recentTaskPackages: List, + ): Array { + val hotseatItems = createHotseatItemsFromPackageUsers(hotseatPackageUsers) + recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) + updateRecentTasks(runningTasks, recentTaskPackages) + return recentAppsController.shownHotseatItems.toTypedArray() + } + + private fun updateRecentTasks(runningTasks: List, recentTaskPackages: List) { + val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages) + val allTasks = + ArrayList().apply { + if (!runningTasks.isEmpty()) { + add(DesktopTask(deskId = 0, DEFAULT_DISPLAY, ArrayList(runningTasks))) + } + addAll(recentTasks) + } + doAnswer { + val callback: Consumer> = it.getArgument(0) + callback.accept(allTasks) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any>>(), any()) + recentTasksChangedListener?.onRecentTasksChanged() + } + + private fun createHotseatItemsFromPackageUsers( + packageUsers: List + ): List { + return packageUsers + .map { + createTestAppInfo(packageName = it.packageName, userHandle = it.userHandle).apply { + container = + if (it.packageName.startsWith("predicted")) { + CONTAINER_HOTSEAT_PREDICTION + } else { + CONTAINER_HOTSEAT + } + } + } + .map { it.makeWorkspaceItem(taskbarActivityContext) } + } + + private fun createTestAppInfo( + packageName: String = "testPackageName", + className: String = "testClassName", + userHandle: UserHandle, + ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) + + private fun createRecentTasksFromPackageNames(packageNames: List): List { + return packageNames.map { packageName -> + if (packageName.startsWith("split")) { + val splitPackages = packageName.split("_") + SplitTask( + createTask(100, splitPackages[0]), + createTask(101, splitPackages[1]), + SplitConfigurationOptions.SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50, + ), + ) + } else { + // Use the number at the end of the test packageName as the id. + val id = 1000 + packageName[packageName.length - 1].code + SingleTask(createTask(id, packageName)) + } + } + } + + private fun createTask( + id: Int, + packageName: String, + isVisible: Boolean = true, + localUserHandle: UserHandle? = null, + ): Task { + return Task( + Task.TaskKey( + id, + WINDOWING_MODE_FREEFORM, + Intent().apply { `package` = packageName }, + ComponentName(packageName, "TestActivity"), + localUserHandle?.identifier ?: myUserHandle.identifier, + 0, + ) + ) + .apply { this.isVisible = isVisible } + } + + private fun setInDesktopMode(inDesktopMode: Boolean) { + whenever(taskbarControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) + .thenReturn(inDesktopMode) + whenever(taskbarControllers.taskbarDesktopModeController.isInDesktopMode(DEFAULT_DISPLAY)) + .thenReturn(inDesktopMode) + } + + private fun createItemInfo( + packageName: String, + userHandle: UserHandle = myUserHandle, + ): ItemInfo { + val appInfo = AppInfo() + appInfo.intent = Intent().setComponent(ComponentName(packageName, "className")) + appInfo.user = userHandle + return WorkspaceItemInfo(appInfo) + } + + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } + + private companion object { + const val HOTSEAT_PACKAGE_1 = "hotseat1" + const val HOTSEAT_PACKAGE_2 = "hotseat2" + const val PREDICTED_PACKAGE_1 = "predicted1" + const val RUNNING_APP_PACKAGE_1 = "running1" + const val RUNNING_APP_PACKAGE_2 = "running2" + const val RUNNING_APP_PACKAGE_3 = "running3" + const val RECENT_PACKAGE_1 = "recent1" + const val RECENT_PACKAGE_2 = "recent2" + const val RECENT_PACKAGE_3 = "recent3" + const val RECENT_SPLIT_PACKAGES_1 = "split1_split2" + } + + data class PackageUser(val packageName: String, val userHandle: UserHandle) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt new file mode 100644 index 000000000..ba53dcdc2 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt @@ -0,0 +1,196 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.SystemUiProxy +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarScrimViewControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams({ + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { + doAnswer { backPressed = true }.whenever(it).onBackEvent(anyOrNull()) + } + }) + ) + + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var scrimViewController: TaskbarScrimViewController + + // Default animation duration. + private val animationDuration = + context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() + + private var backPressed = false + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags(0, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarVisibleWithBubblesExpanded_showsScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + animatorTestRule.advanceTimeBy(animationDuration) + } + + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + } + + @Test + @DisableFlags(FLAG_ENABLE_BUBBLE_BAR) + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarHiddenDuringScrim_hidesScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(GONE) + animatorTestRule.advanceTimeBy(animationDuration) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarOnHomeHiddenDuringScrim_hidesScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + taskbarUnitTestRule.activityContext.bubbleControllers!! + .bubbleStashController + .launcherState = BubbleStashController.BubbleLauncherState.HOME + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(GONE) + animatorTestRule.advanceTimeBy(animationDuration) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_notificationsOverPinnedTaskbarAndBubbles_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags( + SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + true, + ) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarWithBubbleMenu_darkerScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags( + SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, + true, + ) + } + assertThat(scrimViewController.scrimAlpha).isGreaterThan(BUBBLE_EXPANDED_SCRIM_ALPHA) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOnTaskbarVisibilityChanged_stashedTaskbarWithBubbles_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnClick_scrimShown_performsSystemBack() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimView.isClickable).isTrue() + + getInstrumentation().runOnMainSync { scrimViewController.scrimView.performClick() } + assertThat(backPressed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOnClick_scrimHidden_notClickable() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimView.isClickable).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt new file mode 100644 index 000000000..021e1e417 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt @@ -0,0 +1,701 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +import com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION +import com.android.launcher3.R +import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_DEVICE_LOCKED +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IME +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SMALL_SCREEN +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SYSUI +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION_FOR_IME +import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION +import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_DURATION +import com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_STASH +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.TruthJUnit.assume +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EnableFlags(FLAG_ENABLE_BUBBLE_BAR) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarStashControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var stashController: TaskbarStashController + @InjectController lateinit var viewController: TaskbarViewController + @InjectController lateinit var stashedHandleViewController: StashedHandleViewController + @InjectController lateinit var dragLayerController: TaskbarDragLayerController + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var bubbleBarViewController: BubbleBarViewController + @InjectController lateinit var bubbleStashController: BubbleStashController + + private val activityContext by taskbarUnitTestRule::activityContext + + @After fun cancelTimeoutIfExists() = stashController.cancelTimeoutIfExists() + + @Test + @TaskbarMode(TRANSIENT) + fun testInit_transientMode_stashedInApp() { + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testInit_pinnedMode_unstashedInApp() { + assertThat(stashController.isStashedInApp).isFalse() + } + + @Test + @UserSetupMode + @TaskbarMode(PINNED) + fun testInit_userSetupWithPinnedMode_stashedInApp() { + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSetSetupUiVisible_true_stashedInApp() { + getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(true) } + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSetSetupUiVisible_false_unstashedInApp() { + getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(false) } + assertThat(stashController.isStashedInApp).isFalse() + } + + @Test + fun testRecreateAsTransient_timeoutStarted() { + var isPinned by TASKBAR_PINNING.asProperty(context) + isPinned = true + activityContext.controllers.sharedState?.taskbarWasPinned = true + + isPinned = false + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testSupportsVisualStashing_transientMode_supported() { + assertThat(stashController.supportsVisualStashing()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSupportsVisualStashing_pinnedMode_supported() { + assertThat(stashController.supportsVisualStashing()).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testSupportsVisualStashing_threeButtonsMode_unsupported() { + assertThat(stashController.supportsVisualStashing()).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetStashDuration_transientMode() { + assertThat(stashController.stashDuration).isEqualTo(TRANSIENT_TASKBAR_STASH_DURATION) + } + + @Test + @TaskbarMode(PINNED) + fun testGetStashDuration_pinnedMode() { + assertThat(stashController.stashDuration).isEqualTo(PINNED_TASKBAR_TRANSITION_DURATION) + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedInApp_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientInApp_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientNotInApp_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + fun testIsStashed_stashedInLauncherState_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientInOverview_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedInOverviewWithIme_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.updateStateForFlag(FLAG_STASHED_IME, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedTaskbarWithPinnedApp_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.updateStateForFlag(FLAG_STASHED_SYSUI, true) // App pinned. + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + fun testIsInStashedLauncherState_flagUnset_false() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, false) + assertThat(stashController.isInStashedLauncherState).isFalse() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testIsInStashedLauncherState_flagSetInThreeButtonsMode_false() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + assertThat(stashController.isInStashedLauncherState).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsInStashedLauncherState_flagSetInPinnedMode_true() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + assertThat(stashController.isInStashedLauncherState).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testIsTaskbarVisibleAndNotStashing_pinnedButNotVisible_false() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 0f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsTaskbarVisibleAndNotStashing_visibleButStashed_false() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsTaskbarVisibleAndNotStashing_pinnedAndVisible_true() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTouchableHeight_isStashed_stashedHeight() { + assertThat(stashController.touchableHeight).isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTouchableHeight_unstashedTransientMode_heightAndBottomMargin() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false) + stashController.applyState(0) + } + + val expectedHeight = + activityContext.deviceProfile.run { taskbarHeight + taskbarBottomMargin } + assertThat(stashController.touchableHeight).isEqualTo(expectedHeight) + } + + @Test + @TaskbarMode(PINNED) + fun testGetTouchableHeight_pinnedMode_taskbarHeight() { + assertThat(stashController.touchableHeight) + .isEqualTo(activityContext.deviceProfile.taskbarHeight) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetContentHeightToReportToApps_transientMode_stashedHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testGetContentHeightToReportToApps_threeButtonsMode_taskbarHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarHeight) + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_pinnedMode_taskbarHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarHeight) + } + + @Test + @TaskbarMode(PINNED) + @UserSetupMode + fun testGetContentHeightToReportToApps_pinnedInSetupMode_setupWizardInsets() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(context.resources.getDimensionPixelSize(R.dimen.taskbar_suw_insets)) + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_pinnedModeButFolded_stashedHeight() { + getInstrumentation().runOnMainSync { + stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f + stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true) + } + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_homeDisabledWhenFolded_zeroHeight() { + getInstrumentation().runOnMainSync { + stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f + stashedHandleViewController.setIsHomeButtonDisabled(true) + stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true) + } + assertThat(stashController.contentHeightToReportToApps).isEqualTo(0) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTappableHeightToReportToApps_transientMode_zeroHeight() { + assertThat(stashController.tappableHeightToReportToApps).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testGetTappableHeightToReportToApps_pinnedMode_taskbarHeight() { + assertThat(stashController.tappableHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarHeight) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbar_updatesState() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_runUnstashAnimation_startsTaskbarTimeout() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_finishTaskbarTimeout_taskbarStashes() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + getInstrumentation().runOnMainSync { + stashController.timeoutAlarm.finishAlarm() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_autoHideSuspendedForEdu_remainsUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, true) + stashController.updateAndAnimateTransientTaskbar(true) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithBubbles_bubbleBarUnstashes() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.stashBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(false, true) + } + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithoutBubbles_bubbleBarStashed() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.stashBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(false, false) + } + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithBubbles_bubbleBarStashes() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.showBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(true, true) + } + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithoutBubbles_bubbleBarUnstashed() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.showBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(true, false) + } + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_bubbleBarExpandedBeforeTimeout_expandedAfterwards() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleBarViewController.isExpanded = true + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + getInstrumentation().runOnMainSync { + stashController.timeoutAlarm.finishAlarm() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(bubbleBarViewController.isExpanded).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testToggleTaskbarStash_pinnedMode_doesNothing() { + getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_transientMode_unstashesTaskbar() { + getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_twiceInTransientMode_stashesTaskbar() { + getInstrumentation().runOnMainSync { + stashController.toggleTaskbarStash() + stashController.toggleTaskbarStash() + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_notInAppWithTransientMode_doesNothing() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.applyState(0) + stashController.toggleTaskbarStash() + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testAnimateTransientTaskbar_bubblesShownInOverview_stashesTaskbar() { + // Start in Overview. Should unstash Taskbar. + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false) + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + + // Expand bubbles. Should stash Taskbar. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeShown_replacesIconsWithHandle() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + assertThat(viewController.areIconsVisible()).isFalse() + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeHidden_replacesHandleWithIcons() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, true) + animatorTestRule.advanceTimeBy(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + assertThat(viewController.areIconsVisible()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeHidden_verifyAnimationDuration() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + // Start with IME shown. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + // Hide IME with animation. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, false) + // Fast forward without start delay. + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + // Icons should not be visible yet due to start delay. + assertThat(viewController.areIconsVisible()).isFalse() + + // Advance by start delay retroactively. Animation should complete. + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(stashController.taskbarStashStartDelayForIme) + } + assertThat(viewController.areIconsVisible()).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testAnimateThreeButtonsTaskbar_imeShown_hidesIconsAndBg() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + assertThat(viewController.areIconsVisible()).isFalse() + assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(0) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testAnimateThreeButtonsTaskbar_imeHidden_showsIconsAndBg() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, false) + animatorTestRule.advanceTimeBy( + TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme + ) + } + assertThat(viewController.areIconsVisible()).isTrue() + assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(1) + } + + @Test + @TaskbarMode(PINNED) + fun testSetSystemGestureInProgress_whileImeShown_unstashesTaskbar() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + getInstrumentation().runOnMainSync { + stashController.setSystemGestureInProgress(true) + animatorTestRule.advanceTimeBy( + TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme + ) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testSysuiStateImeShowingInApp_hardwareKeyboardWithPinnedMode_notStashedForIme() { + assume().that(activityContext.isHardwareKeyboard).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + } + + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testUnlockTransition_pinnedMode_fadesOutHandle() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true) + stashController.applyState(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false) + stashController.applyState() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUnlockTransition_transientMode_fadesOutHandleEarly() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true) + stashController.applyState(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false) + stashController.applyState() + // Time it takes for just the handle to hide (full stash animation is longer). + animatorTestRule.advanceTimeBy(TRANSIENT_TASKBAR_STASH_ALPHA_DURATION) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + } +} + +private fun TaskbarStashController.updateStateForFlag(flag: Int, value: Boolean) { + updateStateForFlag(flag.toLong(), value) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt deleted file mode 100644 index a999e7f7d..000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt +++ /dev/null @@ -1,164 +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.launcher3.taskbar - -import android.app.Instrumentation -import android.app.PendingIntent -import android.content.IIntentSender -import android.content.Intent -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ServiceTestRule -import com.android.launcher3.LauncherAppState -import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks -import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR -import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric -import com.android.quickstep.AllAppsActionManager -import com.android.quickstep.TouchInteractionService -import com.android.quickstep.TouchInteractionService.TISBinder -import org.junit.Assume.assumeTrue -import org.junit.rules.MethodRule -import org.junit.runners.model.FrameworkMethod -import org.junit.runners.model.Statement - -/** - * Manages the Taskbar lifecycle for unit tests. - * - * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. - * - * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is - * that code that is executed on the main thread in production should also happen on that thread - * when tested. - * - * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test - * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will - * prevent those messages from being processed until after the test body finishes. - * - * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform - * only sections of the test body on the main thread synchronously: - * ``` - * @Test - * fun example() { - * instrumentation.runOnMainSync { doWorkThatPostsMessage() } - * // Second lambda will not execute until message is processed. - * instrumentation.runOnMainSync { verifyMessageResults() } - * } - * ``` - */ -class TaskbarUnitTestRule : MethodRule { - private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val serviceTestRule = ServiceTestRule() - - private lateinit var taskbarManager: TaskbarManager - private lateinit var target: Any - - val activityContext: TaskbarActivityContext - get() { - return taskbarManager.currentActivityContext - ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") - } - - override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement { - return object : Statement() { - override fun evaluate() { - this@TaskbarUnitTestRule.target = target - - val context = instrumentation.targetContext - instrumentation.runOnMainSync { - assumeTrue( - LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent - ) - } - - // Check for existing Taskbar instance from Launcher process. - val launcherTaskbarManager: TaskbarManager? = - if (!isRunningInRobolectric) { - try { - val tisBinder = - serviceTestRule.bindService( - Intent(context, TouchInteractionService::class.java) - ) as? TISBinder - tisBinder?.taskbarManager - } catch (_: Exception) { - null - } - } else { - null - } - - instrumentation.runOnMainSync { - taskbarManager = - TaskbarManager( - context, - AllAppsActionManager(context, UI_HELPER_EXECUTOR) { - PendingIntent(IIntentSender.Default()) - }, - object : TaskbarNavButtonCallbacks {}, - ) - } - - try { - // Replace Launcher Taskbar window with test instance. - instrumentation.runOnMainSync { - launcherTaskbarManager?.removeTaskbarRootViewFromWindow() - taskbarManager.onUserUnlocked() // Required to complete initialization. - } - - injectControllers() - base.evaluate() - } finally { - // Revert Taskbar window. - instrumentation.runOnMainSync { - taskbarManager.destroy() - launcherTaskbarManager?.addTaskbarRootViewToWindow() - } - } - } - } - } - - /** Simulates Taskbar recreation lifecycle. */ - fun recreateTaskbar() { - taskbarManager.recreateTaskbar() - injectControllers() - } - - private fun injectControllers() { - val controllers = activityContext.controllers - val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } - target.javaClass.fields - .filter { it.isAnnotationPresent(InjectController::class.java) } - .forEach { - it.set( - target, - controllerFieldsByType[it.type]?.get(controllers) - ?: throw NoSuchElementException("Failed to find controller for ${it.type}"), - ) - } - } - - /** - * Annotates test controller fields to inject the corresponding controllers from the current - * [TaskbarControllers] instance. - * - * Controllers are injected during test setup and upon calling [recreateTaskbar]. - * - * Multiple controllers can be injected if needed. - */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FIELD) - annotation class InjectController -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt new file mode 100644 index 000000000..b13eafe5b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt @@ -0,0 +1,236 @@ +/* + * 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.launcher3.taskbar + +import android.view.View +import com.android.launcher3.taskbar.TaskbarViewController.DIVIDER_VIEW_POSITION_OFFSET +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +/** + * Legend for the comments below: + * ``` + * A: All Apps Button + * H: Hotseat item + * |: Divider + * R: Recent item + * ``` + * + * The comments are formatted in two lines: + * ``` + * // Items in taskbar, e.g. A | HHHHHH + * // Index of items relative to Hotseat: -1 -.5 012345 + * ``` + */ +class TaskbarViewControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarViewController: TaskbarViewController + + @Test + fun testGetPositionInHotseat_allAppsButton_nonRtl() { + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ 6, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ true, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [>A<] | [HHHHHH] + // -1 -.5 012345 + assertThat(position).isEqualTo(-1) + } + + @Test + fun testGetPositionInHotseat_allAppsButton_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ true, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH] | [>A<] + // 012345 5.5 6 + assertThat(position).isEqualTo(numShownHotseatIcons) + } + + @Test + fun testGetPositionInHotseat_dividerView_notForRecents_nonRtl() { + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ 6, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [A] >|< [HHHHHH] + // -1 -.5 012345 + assertThat(position).isEqualTo(-DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_forRecents_nonRtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ true, + /* recentTaskIndex = */ -1, + ) + // [A] [HHHHHH] >|< [RR] + // -1 012345 5.5 67 + assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_notForRecents_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH] >|< [A] + // 012345 5.5 6 + assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_forRecents_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ true, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH][A] >|< [RR] + // 012345 6 6.5 78 + assertThat(position).isEqualTo(numShownHotseatIcons + DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_recentTasks_firstRecentIndex_nonRtl() { + val numShownHotseatIcons = 6 + val recentTaskIndex = 0 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ recentTaskIndex, + ) + // [A][HHHHHH] | [>RR<] + // -1 012345 5.5 6 7 + assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex) + } + + @Test + fun testGetPositionInHotseat_recentTasks_firstRecentIndex_rtl() { + val numShownHotseatIcons = 6 + val recentTaskIndex = 0 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ recentTaskIndex, + ) + // [HHHHHH][A] | [>RR<] + // 012345 6 6.5 7 8 + assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt new file mode 100644 index 000000000..24ed81f4f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt @@ -0,0 +1,166 @@ +/* + * 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.launcher3.taskbar + +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarDeviceEmulationRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@RunWith(ParameterizedAndroidJunit4::class) +class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) { + + companion object { + @JvmStatic + @Parameters(name = "{0},{1}") + fun getParams(): List> { + val devices = + if (isRunningInRobolectric) { + listOf("pixelFoldable2023", "pixelTablet2023") + } else { + listOf("onDevice") // Unused. + } + val flags = allCombinationsOf(FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION) + return devices.flatMap { d -> flags.map { f -> arrayOf(d, f) } } // Cartesian product. + } + } + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule(flags) + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val deviceEmulationRule = TaskbarDeviceEmulationRule(context, deviceName) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + private lateinit var taskbarView: TaskbarView + + @Before + fun obtainView() { + taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + } + + @Test + fun testUpdateItems_noItems_hasOnlyAllApps() { + runOnMainSync { taskbarView.updateItems(emptyArray(), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS) + } + + @Test + fun testUpdateItems_hotseatItems_hasDividerBetweenAllAppsAndHotseat() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithHotseatItems_hasDividerBetweenHotseatAndAllApps() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_withNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_recentsItems_hasDividerBetweenAllAppsAndRecents() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * 4) + } + + @Test + fun testUpdateItems_hotseatItemsAndRecents_hasDividerBetweenHotseatAndRecents() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 3, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_addHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 2, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_removeHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_addRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_removeRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } + + @Test + fun testUpdateItem_addHotseatItemAfterRecentsItem_hotseatItemBeforeDivider() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt new file mode 100644 index 000000000..92abbbaa0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt @@ -0,0 +1,151 @@ +/* + * 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.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Bitmap.createBitmap +import android.os.Process +import com.android.launcher3.icons.BitmapInfo +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.AppPairInfo +import com.android.launcher3.model.data.FolderInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat + +/** Common utilities for testing [TaskbarView]. */ +object TaskbarViewTestUtil { + + /** Begins an assertion about a [TaskbarView]. */ + fun assertThat(view: TaskbarView): TaskbarViewSubject { + return assertAbout(::TaskbarViewSubject).that(view) + } + + /** Creates an array of fake hotseat items. */ + fun createHotseatItems(size: Int): Array { + return Array(size) { createHotseatWorkspaceItem(it) } + } + + fun createHotseatWorkspaceItem(id: Int = 0): WorkspaceItemInfo { + return WorkspaceItemInfo( + AppInfo(TEST_COMPONENT, "Test App $id", Process.myUserHandle(), Intent()) + ) + .apply { + this.id = id + // Create a placeholder icon so that the test doesn't try to load a high-res icon. + this.bitmap = BitmapInfo.fromBitmap(createBitmap(1, 1, Bitmap.Config.ALPHA_8)) + } + } + + fun createHotseatAppPairsItem(): AppPairInfo { + return AppPairInfo().apply { + add(createHotseatWorkspaceItem(1)) + add(createHotseatWorkspaceItem(2)) + } + } + + fun createHotseatFolderItem(): FolderInfo { + return FolderInfo().apply { + title = "Test Folder" + add(createHotseatWorkspaceItem(1)) + add(createHotseatWorkspaceItem(2)) + add(createHotseatWorkspaceItem(3)) + } + } + + /** Creates a list of fake recent tasks. */ + fun createRecents(size: Int): List { + return List(size) { + SingleTask( + Task().apply { + key = + TaskKey( + it, + 5, + TEST_INTENT, + TEST_COMPONENT, + Process.myUserHandle().identifier, + System.currentTimeMillis(), + ) + } + ) + } + } +} + +/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */ +class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView?) : + Subject(failureMetadata, view) { + + /** Verifies that the types of icons match [expectedTypes] in order. */ + fun hasIconTypes(vararg expectedTypes: TaskbarIconType) { + val actualTypes = + view?.iconViews?.map { + when (it) { + view.allAppsButtonContainer -> ALL_APPS + view.taskbarDividerViewContainer -> DIVIDER + view.taskbarOverflowView -> OVERFLOW + else -> + when (it.tag) { + is ItemInfo -> HOTSEAT + is GroupTask -> RECENT + else -> throw IllegalStateException("Unknown type for $it") + } + } + } + assertThat(actualTypes).containsExactly(*expectedTypes).inOrder() + } + + /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */ + fun hasRecentsOrder(startIndex: Int, expectedIds: List) { + val actualIds = + view?.iconViews?.slice(startIndex.. task.key.id } + } + assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder() + } +} + +/** Types of icons in the [TaskbarView]. */ +enum class TaskbarIconType { + ALL_APPS, + DIVIDER, + HOTSEAT, + RECENT, + OVERFLOW; + + operator fun times(size: Int) = Array(size) { this } +} + +private const val TEST_PACKAGE = "com.android.launcher3.taskbar" +private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "Activity") +private val TEST_INTENT = Intent().apply { `package` = TEST_PACKAGE } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt new file mode 100644 index 000000000..2df4fab45 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt @@ -0,0 +1,352 @@ +/* + * 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.launcher3.taskbar + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.View +import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW +import com.android.launcher3.R +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +@EnableFlags(FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION, FLAG_TASKBAR_OVERFLOW) +class TaskbarViewWithLayoutTransitionTest { + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + private lateinit var taskbarView: TaskbarView + + private val iconViews: Array + get() = taskbarView.iconViews + + private val desktopVisibilityController: DesktopVisibilityController + get() = DesktopVisibilityController.INSTANCE[context] + + private val maxShownRecents: Int + get() = taskbarView.maxNumIconViews - 2 // Account for All Apps and Divider. + + @Before + fun obtainView() { + taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_hotseatItems_hasDividerBetweenHotseatAndAllApps() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_recentsItems_hasDividerBetweenRecentsAndAllApps() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasIconTypes(*RECENT * 4, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_recentsItems_recentsAreReversed() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(3, 2, 1, 0)) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_hotseatItemsAndRecents_hasDividerBetweenRecentsAndHotseat() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) } + assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, *HOTSEAT * 3, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addHotseatItemWithoutRecents_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), emptyList()) + taskbarView.updateItems(createHotseatItems(2), emptyList()) + } + assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addHotseatItemWithRecents_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, *HOTSEAT * 2, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + } + assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + fun testUpdateItems_addRecentsItem_viewAddedOnRight() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + val prevIconViews = iconViews + + val newRecents = createRecents(2) + taskbarView.updateItems(emptyArray(), newRecents) + + assertThat(taskbarView).hasRecentsOrder(startIndex = 2, expectedIds = listOf(0, 1)) + assertThat(iconViews[2]).isSameInstanceAs(prevIconViews[2]) + assertThat(iconViews.last() in prevIconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addRecentsItem_viewAddedOnLeft() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + val prevIconViews = iconViews + + val newRecents = createRecents(2) + taskbarView.updateItems(emptyArray(), newRecents) + + assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(1, 0)) + assertThat(iconViews[1]).isSameInstanceAs(prevIconViews.first()) + assertThat(iconViews.first() in prevIconViews).isFalse() + } + } + + @Test + fun testUpdateItems_removeFirstRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[2] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.first()) + + taskbarView.updateItems(emptyArray(), listOf(recents.last())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + fun testUpdateItems_removeLastRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[3] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.last()) + + taskbarView.updateItems(emptyArray(), listOf(recents.first())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeFirstRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[1] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.first()) + + taskbarView.updateItems(emptyArray(), listOf(recents.last())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeLastRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[0] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.last()) + + taskbarView.updateItems(emptyArray(), listOf(recents.first())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + fun testUpdateItems_desktopMode_hotseatItem_noDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlAndDesktopMode_hotseatItem_noDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) } + assertThat(taskbarView).hasIconTypes(HOTSEAT, ALL_APPS) + } + + @Test + fun testUpdateItems_desktopMode_recentItem_hasDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, RECENT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlAndDesktopMode_recentItem_hasDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_maxRecents_noOverflow() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(maxShownRecents)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * maxShownRecents) + } + + @Test + fun testUpdateItems_moreThanMaxRecents_overflowShownBeforeRecents() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedNumRecents = RECENT * getExpectedNumRecentsWithOverflow() + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, OVERFLOW, *expectedNumRecents) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_moreThanMaxRecents_overflowShownAfterRecents() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow() + assertThat(taskbarView).hasIconTypes(*expectedRecents, OVERFLOW, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_moreThanMaxRecentsWithHotseat_fewerRecentsShown() { + val hotseatSize = 4 + val recentsSize = maxShownRecents + 2 + runOnMainSync { + taskbarView.updateItems(createHotseatItems(hotseatSize), createRecents(recentsSize)) + } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow(hotseatSize) + assertThat(taskbarView) + .hasIconTypes(ALL_APPS, *HOTSEAT * hotseatSize, DIVIDER, OVERFLOW, *expectedRecents) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_moreThanMaxRecentsWithHotseat_fewerRecentsShown() { + val hotseatSize = 4 + val recentsSize = maxShownRecents + 2 + runOnMainSync { + taskbarView.updateItems(createHotseatItems(hotseatSize), createRecents(recentsSize)) + } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow(hotseatSize) + assertThat(taskbarView) + .hasIconTypes(*expectedRecents, OVERFLOW, DIVIDER, *HOTSEAT * hotseatSize, ALL_APPS) + } + + @Test + fun testUpdateItems_moreThanMaxRecents_verifyShownRecentsOrder() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedNumRecents = getExpectedNumRecentsWithOverflow() + assertThat(taskbarView) + .hasRecentsOrder( + startIndex = iconViews.size - expectedNumRecents, + expectedIds = ((recentsSize - expectedNumRecents)..() + + @get:Rule(order = 0) val mockitoRule: MockitoRule = MockitoJUnit.rule() + @get:Rule(order = 1) val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + private lateinit var bubbleBarSwipeController: BubbleBarSwipeController + + @Mock private lateinit var bubbleBarController: BubbleBarController + @Mock private lateinit var bubbleBarViewController: BubbleBarViewController + @Mock private lateinit var bubbleStashController: BubbleStashController + @Mock private lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController + @Mock private lateinit var bubbleDragController: BubbleDragController + @Mock private lateinit var bubbleDismissController: BubbleDismissController + @Mock private lateinit var bubbleBarPinController: BubbleBarPinController + @Mock private lateinit var bubblePinController: BubblePinController + @Mock private lateinit var bubbleCreator: BubbleCreator + + @Before + fun setUp() { + val dimensionProvider = + object : BubbleBarSwipeController.DimensionProvider { + override val unstashThreshold: Int + get() = UNSTASH_THRESHOLD + + override val maxOverscroll: Int + get() = MAX_OVERSCROLL + } + bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider) + + val bubbleControllers = + BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + Optional.of(bubbleBarSwipeController), + bubbleCreator, + ) + + bubbleBarSwipeController.init(bubbleControllers) + } + + // region Test that views have damped translation on swipe + + private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) { + val isUp = swipe < 0 + val damped = OverScroll.dampedScroll(abs(swipe), MAX_OVERSCROLL).toFloat() + val dampedTranslation = if (isUp) -damped else damped + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + } + verify(bubbleStashedHandleViewController).setTranslationYForSwipe(dampedTranslation) + verify(bubbleBarViewController).setTranslationYForSwipe(dampedTranslation) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpCollapsedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + // endregion + + // region Test that translation on views is reset on finish + + private fun testViewsTranslationResetOnFinish(swipe: Float) { + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + bubbleBarSwipeController.finish() + // We use a spring animation. Advance by 5 seconds to give it time to finish + animatorTestRule.advanceTimeBy(5000) + } + val handleSwipeTranslation = argumentCaptor() + val barSwipeTranslation = argumentCaptor() + verify(bubbleStashedHandleViewController, atLeastOnce()) + .setTranslationYForSwipe(handleSwipeTranslation.capture()) + verify(bubbleBarViewController, atLeastOnce()) + .setTranslationYForSwipe(barSwipeTranslation.capture()) + + assertThat(handleSwipeTranslation.firstValue).isNonZero() + assertThat(handleSwipeTranslation.lastValue).isZero() + + assertThat(barSwipeTranslation.firstValue).isNonZero() + assertThat(barSwipeTranslation.lastValue).isZero() + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpCollapsedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + // endregion + + // region Test swipe interactions on stashed bar + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_isSwipeGestureFalse() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse() + } + + @Test + fun swipeUp_stashedBar_overUnstashThreshold_unstashBubbleBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_overUnstashThreshold_isSwipeGestureTrue() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_stashedBar_overUnstashThresholdMultipleTimes_unstashesMultipleTimes() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + verify(bubbleStashController).stashBubbleBar() + + getInstrumentation().runOnMainSync { bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) } + verify(bubbleStashController, times(2)) + .showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_releaseOverUnstashThreshold_expandsBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any()) + getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() } + verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_overUnstashReleaseBelowUnstash_doesNotExpandBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any()) + } + + @Test + fun swipeDown_stashedBar_swipeIgnored() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(DOWN) + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + // endregion + + // region Test swipe interactions on expanded bar + + @Test + fun swipe_expandedBar_swipeIgnored() { + setUpExpandedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.swipeTo(DOWN) + bubbleBarSwipeController.finish() + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + // endregion + + // region Test swipe interactions on collapsed bar + + @Test + fun swipeUp_collapsedBar_doesNotShowBarDuringDrag() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeUp_collapsedBar_belowUnstashThreshold_isSwipeGestureFalse() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse() + } + + @Test + fun swipeUp_collapsedBar_overUnstashThreshold_isSwipeGestureTrue() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_collapsedBar_finishOverUnstashThreshold_expandsBar() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true) + } + + @Test + fun swipeUp_collapsedBar_finishBelowUnstashThreshold_doesNotExpandBar() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeDown_collapsedBar_swipeIgnored() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(DOWN) + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + verify(bubbleStashController, never()).stashBubbleBar() + } + + // endregion + + private fun setUpStashedBar() { + whenever(bubbleStashController.isStashed).thenReturn(true) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpCollapsedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpExpandedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(true) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt new file mode 100644 index 000000000..4ae887718 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt @@ -0,0 +1,97 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.wm.shell.shared.bubbles.BubbleInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleViewTest { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleView: BubbleView + private lateinit var overflowView: BubbleView + private lateinit var bubble: BubbleBarBubble + + @Test + fun hasUnseenContent_bubble() { + setupBubbleViews() + assertThat(bubbleView.hasUnseenContent()).isTrue() + + bubbleView.markSeen() + assertThat(bubbleView.hasUnseenContent()).isFalse() + } + + @Test + fun hasUnseenContent_overflow() { + setupBubbleViews() + assertThat(overflowView.hasUnseenContent()).isFalse() + } + + private fun setupBubbleViews() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val inflater = LayoutInflater.from(context) + + val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) + overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) + + val bubbleInfo = + BubbleInfo( + "key", + 0, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + bitmap, + bitmap, + Color.WHITE, + Path(), + "", + null, + ) + bubbleView.setBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt new file mode 100644 index 000000000..da362bdfb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt @@ -0,0 +1,119 @@ +/* + * 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.launcher3.taskbar.bubbles.animation + +import androidx.core.animation.AnimatorTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleAnimatorTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var bubbleAnimator: BubbleAnimator + + @Test + fun animateNewBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener = listener) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + @Test + fun animateRemovedBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateRemovedBubble( + bubbleIndex = 2, + selectedBubbleIndex = 3, + removingLastBubble = false, + removingLastRemainingBubble = false, + listener, + ) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + @Test + fun animateNewAndRemoveOld_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateNewAndRemoveOld( + selectedBubbleIndex = 3, + newlySelectedBubbleIndex = 2, + removedBubbleIndex = 1, + addedBubbleIndex = 3, + listener, + ) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + private class TestBubbleAnimatorListener : BubbleAnimator.Listener { + + override fun onAnimationUpdate(animatedFraction: Float) {} + + override fun onAnimationCancel() {} + + override fun onAnimationEnd() {} + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt deleted file mode 100644 index cc579abc9..000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +++ /dev/null @@ -1,454 +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.launcher3.taskbar.bubbles.animation - -import android.content.Context -import android.graphics.Color -import android.graphics.Path -import android.graphics.drawable.ColorDrawable -import android.view.LayoutInflater -import android.view.View -import android.view.View.VISIBLE -import android.widget.FrameLayout -import androidx.core.graphics.drawable.toBitmap -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import androidx.test.platform.app.InstrumentationRegistry -import com.android.launcher3.R -import com.android.launcher3.taskbar.bubbles.BubbleBarBubble -import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow -import com.android.launcher3.taskbar.bubbles.BubbleBarView -import com.android.launcher3.taskbar.bubbles.BubbleStashController -import com.android.launcher3.taskbar.bubbles.BubbleView -import com.android.wm.shell.common.bubbles.BubbleInfo -import com.android.wm.shell.shared.animation.PhysicsAnimator -import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@SmallTest -@RunWith(AndroidJUnit4::class) -class BubbleBarViewAnimatorTest { - - private val context = ApplicationProvider.getApplicationContext() - private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler - private lateinit var overflowView: BubbleView - private lateinit var bubbleView: BubbleView - private lateinit var bubble: BubbleBarBubble - private lateinit var bubbleBarView: BubbleBarView - private lateinit var bubbleStashController: BubbleStashController - - @Before - fun setUp() { - animatorScheduler = TestBubbleBarViewAnimatorScheduler() - PhysicsAnimatorTestUtils.prepareForTest() - } - - @Test - fun animateBubbleInForStashed() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(0) - assertThat(handle.translationY) - .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.scaleX).isEqualTo(1) - assertThat(bubbleBarView.scaleY).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - - // execute the hide bubble animation - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(1) - assertThat(handle.translationY).isEqualTo(0) - assertThat(bubbleBarView.alpha).isEqualTo(0) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).stashBubbleBarImmediate() - } - - @Test - fun animateBubbleInForStashed_tapAnimatingBubble() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(0) - assertThat(handle.translationY) - .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.scaleX).isEqualTo(1) - assertThat(bubbleBarView.scaleY).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - - verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion() - - // verify the hide bubble animation is pending - assertThat(animatorScheduler.delayedBlock).isNotNull() - - animator.onBubbleBarTouchedWhileAnimating() - - assertThat(animatorScheduler.delayedBlock).isNull() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - } - - @Test - fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // wait for the animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } - - handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - // verify the hide bubble animation is pending - assertThat(animatorScheduler.delayedBlock).isNotNull() - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.onStashStateChangingWhileAnimating() - } - - // verify that the hide animation was canceled - assertThat(animatorScheduler.delayedBlock).isNull() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) - - // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait - // again - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - handleAnimator.assertIsNotRunning() - } - - @Test - fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - // execute the hide bubble animation - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - // wait for the hide animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - handleAnimator.assertIsRunning() - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.onStashStateChangingWhileAnimating() - } - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) - - // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait - // again - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - handleAnimator.assertIsNotRunning() - } - - @Test - fun animateBubbleInForStashed_showAnimationCanceled() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // wait for the animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } - - handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(animatorScheduler.delayedBlock).isNotNull() - - handleAnimator.cancel() - handleAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(animatorScheduler.delayedBlock).isNull() - } - - @Test - fun animateToInitialState_inApp() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(0) - assertThat(handle.translationY).isEqualTo(0) - assertThat(handle.alpha).isEqualTo(1) - - verify(bubbleStashController).stashBubbleBarImmediate() - } - - @Test - fun animateToInitialState_inApp_autoExpanding() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - verify(bubbleStashController).showBubbleBarImmediate() - } - - @Test - fun animateToInitialState_inHome() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - verify(bubbleStashController).showBubbleBarImmediate() - } - - private fun setUpBubbleBar() { - bubbleBarView = BubbleBarView(context) - InstrumentationRegistry.getInstrumentation().runOnMainSync { - bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) - val inflater = LayoutInflater.from(context) - - val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) - overflowView = - inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView - overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) - bubbleBarView.addView(overflowView) - - val bubbleInfo = - BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false) - bubbleView = - inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView - bubble = - BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "") - bubbleView.setBubble(bubble) - bubbleBarView.addView(bubbleView) - } - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - } - - private fun setUpBubbleStashController() { - bubbleStashController = mock() - whenever(bubbleStashController.isStashed).thenReturn(true) - whenever(bubbleStashController.diffBetweenHandleAndBarCenters) - .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS) - whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation) - .thenReturn(HANDLE_TRANSLATION) - whenever(bubbleStashController.bubbleBarTranslationYForTaskbar) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - } - - private fun PhysicsAnimator.assertIsRunning() { - InstrumentationRegistry.getInstrumentation().runOnMainSync { - assertThat(isRunning()).isTrue() - } - } - - private fun PhysicsAnimator.assertIsNotRunning() { - InstrumentationRegistry.getInstrumentation().runOnMainSync { - assertThat(isRunning()).isFalse() - } - } - - private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { - - var delayedBlock: Runnable? = null - private set - - override fun post(block: Runnable) { - block.run() - } - - override fun postDelayed(delayMillis: Long, block: Runnable) { - check(delayedBlock == null) { "there is already a pending block waiting to run" } - delayedBlock = block - } - - override fun cancel(block: Runnable) { - check(delayedBlock == block) { "the pending block does not match the canceled block" } - delayedBlock = null - } - } -} - -private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f -private const val HANDLE_TRANSLATION = -30f -private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f -private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt new file mode 100644 index 000000000..91fe6a6f0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt @@ -0,0 +1,256 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.content.Context +import android.graphics.Color +import android.graphics.PointF +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [BubbleBarFlyoutController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarFlyoutControllerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var flyoutController: BubbleBarFlyoutController + private lateinit var flyoutContainer: FrameLayout + private lateinit var flyoutCallbacks: FakeFlyoutCallbacks + private val context = ApplicationProvider.getApplicationContext() + private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message") + private var onLeft = true + private var flyoutTy = 50f + + private val showAnimationDuration = 400L + private val hideAnimationDuration = 350L + + @Before + fun setUp() { + flyoutContainer = FrameLayout(context) + val positioner = + object : BubbleBarFlyoutPositioner { + override val isOnLeft + get() = onLeft + + override val targetTy + get() = flyoutTy + + override val distanceToCollapsedPosition = PointF(100f, 200f) + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 50f + } + flyoutCallbacks = FakeFlyoutCallbacks() + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler) + } + + @Test + fun flyoutPosition_left() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) + assertThat(flyout.translationY).isEqualTo(50f) + } + } + + @Test + fun flyoutPosition_right() { + onLeft = false + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) + assertThat(flyout.translationY).isEqualTo(50f) + } + } + + @Test + fun flyoutMessage() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val sender = flyout.findViewById(R.id.bubble_flyout_title) + assertThat(sender.text).isEqualTo("sender name") + val message = flyout.findViewById(R.id.bubble_flyout_text) + assertThat(message.text).isEqualTo("message") + } + } + + @Test + fun hideFlyout_removedFromContainer() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutController.hasFlyout()).isTrue() + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.collapseFlyout {} + animatorTestRule.advanceTimeBy(hideAnimationDuration) + } + assertThat(flyoutContainer.childCount).isEqualTo(0) + assertThat(flyoutController.hasFlyout()).isFalse() + } + + @Test + fun cancelFlyout_fadesOutFlyout() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyoutView = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyoutView.alpha).isEqualTo(1f) + flyoutController.cancelFlyout {} + animatorTestRule.advanceTimeBy(hideAnimationDuration) + assertThat(flyoutView.alpha).isEqualTo(0f) + } + } + + @Test + fun clickFlyout_notifiesCallback() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyoutView = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyoutView.alpha).isEqualTo(1f) + animatorTestRule.advanceTimeBy(showAnimationDuration) + flyoutView.performClick() + } + assertThat(flyoutCallbacks.flyoutClicked).isTrue() + } + + @Test + fun updateFlyoutWhileExpanding() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutController.hasFlyout()).isTrue() + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + // advance the animation about halfway + animatorTestRule.advanceTimeBy(100) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + // set negative translation to verify that the top boundary extends as a result of + // updating while expanding + flyout.translationY = -50f + flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + } + } + + @Test + fun updateFlyoutFullyExpanded() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + animatorTestRule.advanceTimeBy(showAnimationDuration) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + // set negative translation to verify that the top boundary extends as a result of + // updating while fully expanded + flyout.translationY = -50f + flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {} + + // advance the timer so that the fade out animation plays + animatorTestRule.advanceTimeBy(hideAnimationDuration) + assertThat(flyout.alpha).isEqualTo(0) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + + // advance the timer so that the fade in animation plays + animatorTestRule.advanceTimeBy(showAnimationDuration) + assertThat(flyout.alpha).isEqualTo(1) + } + } + + @Test + fun updateFlyoutWhileCollapsing() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + animatorTestRule.advanceTimeBy(showAnimationDuration) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + var flyoutCollapsed = false + flyoutController.collapseFlyout { flyoutCollapsed = true } + // advance the fake timer so that the collapse animation runs for 125ms + animatorTestRule.advanceTimeBy(125) + + // update the flyout in the middle of collapsing, which should start expanding it. + var flyoutReversed = false + flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true } + + // the collapse and expand animations use an emphasized interpolator, so the reverse + // path does not take the same time. advance the timer the by full duration of the show + // animation to ensure it completes + animatorTestRule.advanceTimeBy(showAnimationDuration) + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyout.alpha).isEqualTo(1) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + // verify that we never called the end action on the collapse animation + assertThat(flyoutCollapsed).isFalse() + // verify that we called the end action on the reverse animation + assertThat(flyoutReversed).isTrue() + } + assertThat(flyoutController.hasFlyout()).isTrue() + } + + private fun setupAndShowFlyout() { + flyoutController.setUpAndShowFlyout(flyoutMessage, {}, {}) + } + + class FakeFlyoutCallbacks : FlyoutCallbacks { + + var flyoutClicked = false + + override fun flyoutClicked() { + flyoutClicked = true + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt new file mode 100644 index 000000000..88b39d365 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt @@ -0,0 +1,415 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.AnimatorTestRule +import android.content.Context +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.util.MultiValueAlpha +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Unit tests for [PersistentBubbleStashController]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class PersistentBubbleStashControllerTest { + + companion object { + const val BUBBLE_BAR_HEIGHT = 100f + const val HOTSEAT_VERTICAL_CENTER = 95 + const val HOTSEAT_TRANSLATION_Y = -45f + const val TASK_BAR_TRANSLATION_Y = -5f + } + + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + + @Mock lateinit var bubbleBarViewController: BubbleBarViewController + + @Mock lateinit var taskbarInsetsController: TaskbarInsetsController + + private lateinit var persistentTaskBarStashController: PersistentBubbleStashController + private lateinit var translationY: AnimatedFloat + private lateinit var scale: AnimatedFloat + private lateinit var alpha: MultiValueAlpha + + @Before + fun setUp() { + persistentTaskBarStashController = + PersistentBubbleStashController(DefaultDimensionsProvider()) + setUpBubbleBarView() + setUpBubbleBarController() + persistentTaskBarStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER + persistentTaskBarStashController.init( + taskbarInsetsController, + bubbleBarViewController, + null, + ImmediateAction(), + ) + } + + @Test + fun updateLauncherState_noBubbles_controllerNotified() { + // Given bubble bar has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + + // When switch to home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + } + + // Then bubble bar view controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false) + } + + @Test + fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + } + + // Then translation Y is animating and the bubble bar controller is notified + assertThat(translationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + // Check translation Y is correct and the insets controller is notified + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch to home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + } + + // Then translation Y is animating and the bubble bar controller is notified + assertThat(translationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + + // Check translation Y is correct and the insets controller is notified + assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y) + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() { + // Given bubble bar is on overview + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + clearInvocations(bubbleBarViewController) + + // When switch out of the overview screen + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Then bubble bar controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() { + // When switch to the overview screen + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + + // Then bubble bar controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + } + + @Test + fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() { + // Given screen is locked and bubble bar has bubbles + persistentTaskBarStashController.isSysuiLocked = true + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch to the overview screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.isSysuiLocked = false + } + + // Then + assertThat(translationY.isAnimating).isTrue() + assertThat(scale.isAnimating).isTrue() + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + + // Then bubble bar is fully visible at the correct location + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBarImmediateToY() { + // Given bubble bar is fully transparent and scaled to 0 at 0 y position + val targetY = 341f + bubbleBarView.alpha = 0f + bubbleBarView.scaleX = 0f + bubbleBarView.scaleY = 0f + bubbleBarView.translationY = 0f + + // When + persistentTaskBarStashController.showBubbleBarImmediate(targetY) + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(targetY) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + } + + @Test + fun isTransientTaskbar_false() { + assertThat(persistentTaskBarStashController.isTransientTaskBar).isFalse() + } + + @Test + fun hasHandleView_false() { + assertThat(persistentTaskBarStashController.hasHandleView).isFalse() + } + + @Test + fun isStashed_false() { + assertThat(persistentTaskBarStashController.isStashed).isFalse() + } + + @Test + fun bubbleBarTranslationYForTaskbar() { + // Give bubble bar is on home + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(HOTSEAT_TRANSLATION_Y) + + // Give bubble bar is not on home + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesTranslationFromHomeToInApp() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(HOTSEAT_TRANSLATION_Y) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + + val middleBetweenHotseatAndTaskbar = (HOTSEAT_TRANSLATION_Y + TASK_BAR_TRANSLATION_Y) / 2f + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isWithin(0.1f) + .of(middleBetweenHotseatAndTaskbar) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesOne() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + // Insets are not updated for values between 0 and 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + + // Update insets when progress reaches 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesZero() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + // Insets are not updated for values between 0 and 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + + // Update insets when progress reaches 0 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun inAppDisplayOverrideProgress_onHome_cancelExistingAnimation() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + bubbleBarViewController.bubbleBarTranslationY.animateToValue(100f) + advanceTimeBy(10) + assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isTrue() + + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + } + assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isFalse() + } + + @Test + fun inAppDisplayProgressUpdate_inApp_noTranslationUpdate() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_inApp_noInsetsUpdate() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f + + // Never triggers an update to insets + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBar_expand_bubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = true, + bubbleBarGesture = true, + ) + + verify(bubbleBarViewController).setExpanded(true, true) + } + + @Test + fun showBubbleBar_expand_notBubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = true, + bubbleBarGesture = false, + ) + + verify(bubbleBarViewController).setExpanded(true, false) + } + + @Test + fun showBubbleBar_notExpanding_bubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = false, + bubbleBarGesture = true, + ) + + verify(bubbleBarViewController, never()).setExpanded(any(), any()) + } + + private fun advanceTimeBy(advanceMs: Long) { + // Advance animator for on-device tests + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) } + } + + private fun setUpBubbleBarView() { + getInstrumentation().runOnMainSync { + bubbleBarView = BubbleBarView(context) + bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) + } + } + + private fun setUpBubbleBarController() { + translationY = AnimatedFloat(Runnable { bubbleBarView.translationY = translationY.value }) + scale = + AnimatedFloat( + Runnable { + val scale: Float = scale.value + bubbleBarView.scaleX = scale + bubbleBarView.scaleY = scale + } + ) + alpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */) + + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY) + whenever(bubbleBarViewController.bubbleBarScaleY).thenReturn(scale) + whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha) + whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt new file mode 100644 index 000000000..96c2f4554 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt @@ -0,0 +1,35 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +class ImmediateAction : BubbleStashController.ControllersAfterInitAction { + override fun runAfterInit(action: Runnable) = action.run() +} + +class DefaultDimensionsProvider( + private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE, + private val taskBarHeight: Int = TASKBAR_HEIGHT, +) : BubbleStashController.TaskbarHotseatDimensionsProvider { + override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace + + override fun getTaskbarHeight(): Int = taskBarHeight + + companion object { + const val TASKBAR_BOTTOM_SPACE = 0 + const val TASKBAR_HEIGHT = 110 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt new file mode 100644 index 000000000..b24926bec --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt @@ -0,0 +1,622 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.AnimatorSet +import android.animation.AnimatorTestRule +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.StashedHandleView +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.TaskbarStashController +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.util.MultiValueAlpha +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Unit tests for [TransientBubbleStashController]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class TransientBubbleStashControllerTest { + + companion object { + const val TASKBAR_BOTTOM_SPACE = 5 + const val HOTSEAT_VERTICAL_CENTER = 95 + const val BUBBLE_BAR_WIDTH = 200 + const val BUBBLE_BAR_HEIGHT = 100 + const val HOTSEAT_TRANSLATION_Y = -45f + const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE.toFloat() + const val HANDLE_VIEW_WIDTH = 150 + const val HANDLE_VIEW_HEIGHT = 4 + const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -4.5f + } + + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() + + @Mock lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController + + @Mock lateinit var bubbleBarViewController: BubbleBarViewController + + @Mock lateinit var taskbarInsetsController: TaskbarInsetsController + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + private lateinit var stashedHandleView: StashedHandleView + private lateinit var bubbleView: BubbleView + private lateinit var barTranslationY: AnimatedFloat + private lateinit var barScaleX: AnimatedFloat + private lateinit var barScaleY: AnimatedFloat + private lateinit var barAlpha: MultiValueAlpha + private lateinit var bubbleOffsetY: AnimatedFloat + private lateinit var bubbleAlpha: AnimatedFloat + private lateinit var backgroundAlpha: AnimatedFloat + private lateinit var stashedHandleAlpha: MultiValueAlpha + private lateinit var stashedHandleScale: AnimatedFloat + private lateinit var stashedHandleTranslationY: AnimatedFloat + private lateinit var stashPhysicsAnimator: PhysicsAnimator + + private lateinit var mTransientBubbleStashController: TransientBubbleStashController + + @Before + fun setUp() { + val taskbarHotseatDimensionsProvider = + DefaultDimensionsProvider(taskBarBottomSpace = TASKBAR_BOTTOM_SPACE) + mTransientBubbleStashController = + TransientBubbleStashController(taskbarHotseatDimensionsProvider, context) + setUpBubbleBarView() + setUpBubbleBarController() + setUpStashedHandleView() + setUpBubbleStashedHandleViewController() + PhysicsAnimatorTestUtils.prepareForTest() + mTransientBubbleStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER + mTransientBubbleStashController.init( + taskbarInsetsController, + bubbleBarViewController, + bubbleStashedHandleViewController, + ImmediateAction(), + ) + } + + @Test + fun updateLauncherState_noBubbles_controllerNotified() { + // Given bubble bar has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + + // When switch to home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME + } + + // Then bubble bar view controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false) + } + + @Test + fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME + } + + // Then BubbleBarView is animating, BubbleBarViewController controller is notified + assertThat(barTranslationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // Then translation Y is correct and the insets controller is notified + assertThat(barTranslationY.isAnimating).isFalse() + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on overview and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + } + + // Then BubbleBarView is animating, BubbleBarViewController controller is notified + assertThat(barTranslationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // Then translation Y is correct and the insets controller is notified + assertThat(barTranslationY.isAnimating).isFalse() + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_unstashes() { + // Given bubble bar is stashed with bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + assertThat(mTransientBubbleStashController.isStashed).isTrue() + + // Move to overview + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + } + // No longer stashed in overview + assertThat(mTransientBubbleStashController.isStashed).isFalse() + } + + @Test + fun updateStashedAndExpandedState_stashAndCollapse_bubbleBarHidden_stashedHandleShown() { + // Given bubble bar has bubbles and not stashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + val bubbleInitialTranslation = bubbleView.translationY + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Wait until animations ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // Then check BubbleBarController is notified + verify(bubbleBarViewController).onStashStateChanging() + // Bubble bar is stashed + assertThat(mTransientBubbleStashController.isStashed).isTrue() + assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(0f) + assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX()) + assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY()) + assertThat(bubbleBarView.background.alpha).isEqualTo(255) + // Handle view is visible + assertThat(stashedHandleView.translationY).isEqualTo(0) + assertThat(stashedHandleView.alpha).isEqualTo(1) + // Bubble view is reset + assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation) + assertThat(bubbleView.alpha).isEqualTo(1f) + } + + @Test + fun updateStashedAndExpandedState_unstash_bubbleBarShown_stashedHandleHidden() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + val bubbleInitialTranslation = bubbleView.translationY + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Wait until animations ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // Then check BubbleBarController is notified + verify(bubbleBarViewController).onStashStateChanging() + // Bubble bar is unstashed + assertThat(mTransientBubbleStashController.isStashed).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.background.alpha).isEqualTo(255) + // Handle view is hidden + assertThat(stashedHandleView.translationY).isEqualTo(0) + assertThat(stashedHandleView.alpha).isEqualTo(0) + // Bubble view is reset + assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation) + assertThat(bubbleView.alpha).isEqualTo(1f) + } + + @Test + fun updateStashedAndExpandedState_stash_animatesAlphaForBubblesAndBackgroundSeparately() { + // Given bubble bar has bubbles and is unstashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Stop after alpha starts + advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10) + + // Bubble bar alpha is set to 1 + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // We animate alpha for background and children separately + assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f)) + assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255)) + assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt()) + } + + @Test + fun updateStashedAndExpandedState_unstash_animatesAlphaForBubblesAndBackgroundSeparately() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Stop after alpha starts + advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10) + + // Bubble bar alpha is set to 1 + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // We animate alpha for background and children separately + assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f)) + assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255)) + assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt()) + } + + @Test + fun updateStashedAndExpandedState_stash_updateBarVisibilityAfterAnimation() { + // Given bubble bar has bubbles and is unstashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Hides bubble bar only after animation completes + verify(bubbleBarViewController, never()).setHiddenForStashed(true) + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + verify(bubbleBarViewController).setHiddenForStashed(true) + } + + @Test + fun updateStashedAndExpandedState_unstash_updateBarVisibilityBeforeAnimation() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Shows bubble bar immediately + verify(bubbleBarViewController).setHiddenForStashed(false) + } + + @Test + fun updateStashedAndExpandedState_expand_bubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = true, + bubbleBarGesture = true, + ) + } + + verify(bubbleBarViewController).setExpanded(true, true) + } + + @Test + fun updateStashedAndExpandedState_expand_notBubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = true, + bubbleBarGesture = false, + ) + } + + verify(bubbleBarViewController).setExpanded(true, false) + } + + @Test + fun updateStashedAndExpandedState_notExpanding_bubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + bubbleBarGesture = true, + ) + } + + verify(bubbleBarViewController, never()).setExpanded(any(), any()) + } + + @Test + fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() { + // Given screen is locked and bubble bar has bubbles + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.isSysuiLocked = true + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + } + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + + // When switch to the overview screen + getInstrumentation().runOnMainSync { mTransientBubbleStashController.isSysuiLocked = false } + + // Then + assertThat(barTranslationY.isAnimating).isTrue() + assertThat(barScaleX.isAnimating).isTrue() + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + + // Then bubble bar is fully visible at the correct location + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.translationY) + .isEqualTo(PersistentBubbleStashControllerTest.TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // Insets controller is notified + verify(taskbarInsetsController, atLeastOnce()) + .onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBarImmediateToY() { + // Given bubble bar is fully transparent and scaled to 0 at 0 y position + val targetY = 341f + bubbleBarView.alpha = 0f + bubbleBarView.scaleX = 0f + bubbleBarView.scaleY = 0f + bubbleBarView.translationY = 0f + stashedHandleView.translationY = targetY + + // When + mTransientBubbleStashController.showBubbleBarImmediate(targetY) + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(targetY) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + // Handle is transparent + assertThat(stashedHandleView.alpha).isEqualTo(0) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + // Bubble bar visibility updated + verify(bubbleBarViewController).setHiddenForStashed(false) + } + + @Test + fun stashBubbleBarImmediate() { + // When + mTransientBubbleStashController.stashBubbleBarImmediate() + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX()) + assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY()) + // Handle is visible at correct Y position + assertThat(stashedHandleView.alpha).isEqualTo(1) + assertThat(stashedHandleView.translationY).isEqualTo(0) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + // Bubble bar visibility updated + verify(bubbleBarViewController).setHiddenForStashed(true) + } + + @Test + fun getTouchableHeight_stashed_stashHeightReturned() { + // When + mTransientBubbleStashController.isStashed = true + val height = mTransientBubbleStashController.getTouchableHeight() + + // Then + assertThat(height).isEqualTo(HANDLE_VIEW_HEIGHT) + } + + @Test + fun getTouchableHeight_unstashed_barHeightReturned() { + // When BubbleBar is not stashed + mTransientBubbleStashController.isStashed = false + val height = mTransientBubbleStashController.getTouchableHeight() + + // Then bubble bar height is returned + assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT) + } + + @Test + fun getHandleViewAlpha_stashedHasBubbles_alphaPropertyReturned() { + // Given BubbleBar is stashed and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + mTransientBubbleStashController.isStashed = true + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should not be null + assertThat(alphaProperty).isNotNull() + } + + @Test + fun getHandleViewAlpha_stashedHasNoBubblesBar_alphaPropertyIsNull() { + // Given BubbleBar is stashed and has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + mTransientBubbleStashController.isStashed = true + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should be null + assertThat(alphaProperty).isNull() + } + + @Test + fun getHandleViewAlpha_unstashedHasBubbles_alphaPropertyIsNull() { + // Given BubbleBar is not stashed and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + mTransientBubbleStashController.isStashed = false + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should be null + assertThat(alphaProperty).isNull() + } + + private fun advanceTimeBy(advanceMs: Long) { + // Advance animator for on-device tests + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) } + } + + private fun setUpBubbleBarView() { + getInstrumentation().runOnMainSync { + bubbleBarView = BubbleBarView(context) + bubbleBarView.layoutParams = + FrameLayout.LayoutParams(BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT) + bubbleView = BubbleView(context) + bubbleBarView.addBubble(bubbleView) + bubbleBarView.layout(0, 0, BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT) + } + } + + private fun setUpStashedHandleView() { + getInstrumentation().runOnMainSync { + stashedHandleView = StashedHandleView(context) + stashedHandleView.layoutParams = + FrameLayout.LayoutParams(HANDLE_VIEW_WIDTH, HANDLE_VIEW_HEIGHT) + } + } + + private fun setUpBubbleBarController() { + barTranslationY = + AnimatedFloat(Runnable { bubbleBarView.translationY = barTranslationY.value }) + bubbleOffsetY = AnimatedFloat { value -> bubbleBarView.setBubbleOffsetY(value) } + barScaleX = AnimatedFloat { value -> bubbleBarView.scaleX = value } + barScaleY = AnimatedFloat { value -> bubbleBarView.scaleY = value } + barAlpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */) + bubbleAlpha = AnimatedFloat { value -> bubbleBarView.setBubbleAlpha(value) } + backgroundAlpha = AnimatedFloat { value -> bubbleBarView.setBackgroundAlpha(value) } + + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(barTranslationY) + whenever(bubbleBarViewController.bubbleOffsetY).thenReturn(bubbleOffsetY) + whenever(bubbleBarViewController.bubbleBarBackgroundScaleX).thenReturn(barScaleX) + whenever(bubbleBarViewController.bubbleBarBackgroundScaleY).thenReturn(barScaleY) + whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(barAlpha) + whenever(bubbleBarViewController.bubbleBarBubbleAlpha).thenReturn(bubbleAlpha) + whenever(bubbleBarViewController.bubbleBarBackgroundAlpha).thenReturn(backgroundAlpha) + whenever(bubbleBarViewController.bubbleBarCollapsedWidth) + .thenReturn(BUBBLE_BAR_WIDTH.toFloat()) + whenever(bubbleBarViewController.bubbleBarCollapsedHeight) + .thenReturn(BUBBLE_BAR_HEIGHT.toFloat()) + whenever(bubbleBarViewController.createRevealAnimatorForStashChange(any())) + .thenReturn(AnimatorSet()) + } + + private fun setUpBubbleStashedHandleViewController() { + stashedHandleTranslationY = + AnimatedFloat(Runnable { stashedHandleView.translationY = barTranslationY.value }) + stashedHandleScale = + AnimatedFloat( + Runnable { + val scale: Float = barScaleX.value + bubbleBarView.scaleX = scale + bubbleBarView.scaleY = scale + } + ) + stashedHandleAlpha = MultiValueAlpha(stashedHandleView, 1 /* num alpha channels */) + stashPhysicsAnimator = PhysicsAnimator.getInstance(stashedHandleView) + whenever(bubbleStashedHandleViewController.stashedHandleAlpha) + .thenReturn(stashedHandleAlpha) + whenever(bubbleStashedHandleViewController.physicsAnimator).thenReturn(stashPhysicsAnimator) + whenever(bubbleStashedHandleViewController.stashedWidth).thenReturn(HANDLE_VIEW_WIDTH) + whenever(bubbleStashedHandleViewController.stashedHeight).thenReturn(HANDLE_VIEW_HEIGHT) + whenever(bubbleStashedHandleViewController.setTranslationYForSwipe(any())).thenAnswer { + invocation -> + (invocation.arguments[0] as Float).also { stashedHandleView.translationY = it } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt index eebd8f9f1..1113129ba 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt @@ -18,19 +18,19 @@ package com.android.launcher3.taskbar.overlay import android.app.ActivityManager.RunningTaskInfo import android.view.MotionEvent -import androidx.test.annotation.UiThreadTest -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY import com.android.launcher3.AbstractFloatingView.hasOpenView import com.android.launcher3.taskbar.TaskbarActivityContext -import com.android.launcher3.taskbar.TaskbarUnitTestRule -import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.android.launcher3.views.BaseDragLayer +import com.android.launcher3.util.TestUtil.getOnUiThread import com.android.systemui.shared.system.TaskStackChangeListeners import com.google.common.truth.Truth.assertThat import org.junit.Rule @@ -41,193 +41,185 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023"]) class TaskbarOverlayControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule() + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var overlayController: TaskbarOverlayController private val taskbarContext: TaskbarActivityContext get() = taskbarUnitTestRule.activityContext @Test - @UiThreadTest fun testRequestWindow_twice_reusesWindow() { - val context1 = overlayController.requestWindow() - val context2 = overlayController.requestWindow() + val (context1, context2) = + getOnUiThread { + Pair(overlayController.requestWindow(), overlayController.requestWindow()) + } assertThat(context1).isSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() { - val context1 = overlayController.requestWindow() - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + runOnMainSync { overlayController.hideWindow() } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingOverlay_createsNewWindow() { - val context1 = overlayController.requestWindow() - TestOverlayView.show(context1) - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + runOnMainSync { + TestOverlayView.show(context1) + overlayController.hideWindow() + } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_addsProxyView() { - TestOverlayView.show(overlayController.requestWindow()) + runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testRequestWindow_closeProxyView_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { + AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + } assertThat(overlay.isOpen).isFalse() } @Test fun testRequestWindow_attachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } - + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Allow drag layer to attach before checking. - getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() } + runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() } } @Test - @UiThreadTest fun testHideWindow_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - overlayController.hideWindow() + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { overlayController.hideWindow() } assertThat(overlay.isOpen).isFalse() } @Test fun testHideWindow_detachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Wait for drag layer to be attached to window before hiding. - getInstrumentation().runOnMainSync { + runOnMainSync { overlayController.hideWindow() assertThat(dragLayer.isAttachedToWindow).isFalse() } } @Test - @UiThreadTest fun testTwoOverlays_closeOne_windowStaysOpen() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) + runOnMainSync { overlay1.close(false) } assertThat(overlay2.isOpen).isTrue() assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testTwoOverlays_closeAll_closesWindow() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) - overlay2.close(false) + runOnMainSync { + overlay1.close(false) + overlay2.close(false) + } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() } @Test - @UiThreadTest fun testRecreateTaskbar_closesWindow() { - TestOverlayView.show(overlayController.requestWindow()) + runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) } taskbarUnitTestRule.recreateTaskbar() assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() } @Test fun testTaskMovedToFront_closesOverlay() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - } - + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo()) // Make sure TaskStackChangeListeners' Handler posts the callback before checking state. - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + runOnMainSync { assertThat(overlay.isOpen).isFalse() } } @Test fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - taskbarContext.controllers.sharedState?.allAppsVisible = false - } + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = false } TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isTrue() } + runOnMainSync { assertThat(overlay.isOpen).isTrue() } } @Test fun testTaskStackChanged_allAppsOpen_closesOverlay() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - taskbarContext.controllers.sharedState?.allAppsVisible = true - } + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = true } TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + runOnMainSync { assertThat(overlay.isOpen).isFalse() } } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isFalse() } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isTrue() } private class TestOverlayView - private constructor( - private val overlayContext: TaskbarOverlayContext, - ) : AbstractFloatingView(overlayContext, null) { + private constructor(private val overlayContext: TaskbarOverlayContext) : + AbstractFloatingView(overlayContext, null) { var type = TYPE_OPTIONS_POPUP diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt new file mode 100644 index 000000000..5f7b360bd --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.TaskIconCache +import com.android.quickstep.TaskThumbnailCache +import com.android.quickstep.util.GroupTask +import java.util.function.Consumer +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +/** Helper class to mock the [RecentsModel] object in test */ +class MockedRecentsModelHelper { + private val mockIconCache: TaskIconCache = mock() + private val mockThumbnailCache: TaskThumbnailCache = mock() + + var taskListId = 0 + var recentTasksChangedListener: RecentTasksChangedListener? = null + var taskRequests: MutableList<(List) -> Unit> = mutableListOf() + + val mockRecentsModel: RecentsModel = mock { + on { iconCache } doReturn mockIconCache + + on { thumbnailCache } doReturn mockThumbnailCache + + on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null } + + on { registerRecentTasksChangedListener(any()) } doAnswer + { + recentTasksChangedListener = it.getArgument(0) + } + + on { getTasks(anyOrNull(), anyOrNull()) } doAnswer + { + val request = it.getArgument>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { getTasks(anyOrNull()) } doAnswer + { + val request = it.getArgument>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt new file mode 100644 index 000000000..359b87618 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt @@ -0,0 +1,50 @@ +/* + * 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.launcher3.taskbar.rules + +import com.android.quickstep.util.GroupTask +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class MockedRecentsModelTestRule(private val modelHelper: MockedRecentsModelHelper) : TestRule { + private var recentTasks: List = emptyList() + + override fun apply(base: Statement?, description: Description?): Statement { + return object : Statement() { + override fun evaluate() { + base?.evaluate() + } + } + } + + // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so + // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list. + fun updateRecentTasks(tasks: List) { + ++modelHelper.taskListId + recentTasks = tasks + modelHelper.recentTasksChangedListener?.onRecentTasksChanged() + } + + fun resolvePendingTaskRequests() { + val requests = mutableListOf<(List) -> Unit>() + requests.addAll(modelHelper.taskRequests) + modelHelper.taskRequests.clear() + + requests.forEach { it(recentTasks) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt new file mode 100644 index 000000000..f22580719 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.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. + */ + +package com.android.launcher3.taskbar.rules + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.NavigationMode +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy + +/** + * Allows tests to specify which Taskbar [Mode] to run under. + * + * [context] should match the test's target context, so that Dagger singleton instances are properly + * sandboxed. + * + * Annotate tests with [TaskbarMode] to set a mode. If the annotation is omitted for any tests, this + * rule is a no-op. + * + * Make sure this rule precedes any rules that depend on [DisplayController], or else the instance + * might be inconsistent across the test lifecycle. + */ +class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRule { + /** The selected Taskbar mode. */ + enum class Mode { + TRANSIENT, + PINNED, + THREE_BUTTONS, + } + + /** Overrides Taskbar [mode] for a test. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + annotation class TaskbarMode(val mode: Mode) + + override fun apply(base: Statement, description: Description): Statement { + val taskbarMode = description.getAnnotation(TaskbarMode::class.java) ?: return base + + return object : Statement() { + override fun evaluate() { + val mode = taskbarMode.mode + + getInstrumentation().runOnMainSync { + DisplayController.INSTANCE[context].let { + if (it is DisplayControllerSpy) { + it.infoModifier = { info -> + spy(info) { + on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT) + on { isPinnedTaskbar } doReturn (mode == Mode.PINNED) + on { navigationMode } doReturn + when (mode) { + Mode.TRANSIENT, + Mode.PINNED -> NavigationMode.NO_BUTTON + Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS + } + } + } + } + } + } + + base.evaluate() + } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt new file mode 100644 index 000000000..0dd1324c0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt @@ -0,0 +1,88 @@ +/* + * 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.launcher3.taskbar.rules + +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.NavigationMode +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarModeRuleTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarMode_transient_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isTrue() + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + assertThat(DisplayController.getNavigationMode(context)).isEqualTo(NavigationMode.NO_BUTTON) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarMode_transient_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.isTransientTaskbar).isTrue() + assertThat(dp.isGestureMode).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarMode_pinned_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isFalse() + assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() + assertThat(DisplayController.getNavigationMode(context)).isEqualTo(NavigationMode.NO_BUTTON) + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarMode_pinned_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.isTransientTaskbar).isFalse() + assertThat(dp.isGestureMode).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testTaskbarMode_threeButtons_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isFalse() + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + assertThat(DisplayController.getNavigationMode(context)) + .isEqualTo(NavigationMode.THREE_BUTTONS) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testTaskbarMode_threeButtons_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.isTransientTaskbar).isFalse() + assertThat(dp.isGestureMode).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt new file mode 100644 index 000000000..19c88240d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt @@ -0,0 +1,215 @@ +/* + * 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.launcher3.taskbar.rules + +import android.app.Instrumentation +import android.app.PendingIntent +import android.content.IIntentSender +import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE +import android.provider.Settings.Secure.USER_SETUP_COMPLETE +import android.provider.Settings.Secure.getUriFor +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.LauncherAppState +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarControllers +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks +import com.android.launcher3.taskbar.TaskbarUIController +import com.android.launcher3.taskbar.bubbles.BubbleControllers +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.TestUtil +import com.android.quickstep.AllAppsActionManager +import com.android.quickstep.fallback.window.RecentsDisplayModel +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType +import java.util.Locale +import java.util.Optional +import org.junit.Assume.assumeTrue +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Manages the Taskbar lifecycle for unit tests. + * + * Tests should pass in themselves as [testInstance]. They also need to provide their target + * [context] through the constructor. + * + * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. + * + * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is + * that code that is executed on the main thread in production should also happen on that thread + * when tested. + * + * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the + * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run + * code that requires the main thread using something like [Instrumentation.runOnMainSync] or + * [TestUtil.getOnUiThread]. + * + * ``` + * @Test + * fun example() { + * instrumentation.runOnMainSync { doWorkThatPostsMessage() } + * // Second lambda will not execute until message is processed. + * instrumentation.runOnMainSync { verifyMessageResults() } + * } + * ``` + */ +class TaskbarUnitTestRule( + private val testInstance: Any, + private val context: TaskbarWindowSandboxContext, + private val controllerInjectionCallback: () -> Unit = {}, +) : TestRule { + + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + private lateinit var taskbarManager: TaskbarManager + + val activityContext: TaskbarActivityContext + get() { + return taskbarManager.currentActivityContext + ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") + } + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + + // Only run test when Taskbar is enabled. + instrumentation.runOnMainSync { + assumeTrue( + LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent + ) + } + + // Process secure setting annotations. + context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] = + if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1 + context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] = + if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0 + + taskbarManager = + TestUtil.getOnUiThread { + object : + TaskbarManager( + context, + AllAppsActionManager(context, UI_HELPER_EXECUTOR) { + PendingIntent(IIntentSender.Default()) + }, + object : TaskbarNavButtonCallbacks {}, + RecentsDisplayModel.INSTANCE.get(context), + ) { + override fun recreateTaskbars() { + super.recreateTaskbars() + if (currentActivityContext != null) { + injectControllers() + // TODO(b/346394875): we should test a non-default uiController. + activityContext.setUIController(TaskbarUIController.DEFAULT) + controllerInjectionCallback.invoke() + } + } + } + } + + if (description.getAnnotation(ForceRtl::class.java) != null) { + // Needs to be set on window context instead of sandbox context, because it does + // does not propagate between them. However, this change will impact created + // TaskbarActivityContext instances, since they wrap the window context. + // TODO: iterate through all window contexts and do this. + taskbarManager.primaryWindowContext.resources.configuration.setLayoutDirection( + RTL_LOCALE + ) + } + + try { + // Required to complete initialization. + instrumentation.runOnMainSync { taskbarManager.onUserUnlocked() } + + base.evaluate() + } finally { + instrumentation.runOnMainSync { taskbarManager.destroy() } + } + } + } + } + + /** Simulates Taskbar recreation lifecycle. */ + fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbars() } + + private fun injectControllers() { + val bubbleControllerTypes = + BubbleControllers::class.java.fields.map { f -> + if (f.type == Optional::class.java) { + (f.genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + } else { + f.type + } + } + testInstance.javaClass.fields + .filter { it.isAnnotationPresent(InjectController::class.java) } + .forEach { + val controllers: Any = + if (it.type in bubbleControllerTypes) { + activityContext.controllers.bubbleControllers.orElseThrow { + NoSuchElementException("Bubble controllers are not initialized") + } + } else { + activityContext.controllers + } + injectController(it, testInstance, controllers) + } + } + + private fun injectController(field: Field, testInstance: Any, controllers: Any) { + val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } + field.set( + testInstance, + controllerFieldsByType[field.type]?.get(controllers) + ?: throw NoSuchElementException("Failed to find controller for ${field.type}"), + ) + } + + /** + * Annotates test controller fields to inject the corresponding controllers from the current + * [TaskbarControllers] instance. + * + * Controllers are injected during test setup and upon calling [recreateTaskbar]. + * + * Multiple controllers can be injected if needed. + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FIELD) + annotation class InjectController + + /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class UserSetupMode + + /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class NavBarKidsMode + + /** Forces RTL UI for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class ForceRtl +} + +private val RTL_LOCALE = Locale.of("ar", "XB") diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt new file mode 100644 index 000000000..b8b0b5d1e --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt @@ -0,0 +1,238 @@ +/* + * 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.launcher3.taskbar.rules + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarKeyguardController +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.taskbar.TaskbarStashController +import com.android.launcher3.taskbar.bubbles.BubbleBarController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.wm.shell.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Rule +import org.junit.Test +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.model.Statement + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarUnitTestRuleTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val setFlagsRule = SetFlagsRule() + + @Test + fun testSetup_taskbarInitialized() { + onSetup { assertThat(activityContext).isInstanceOf(TaskbarActivityContext::class.java) } + } + + @Test + fun testRecreateTaskbar_activityContextChanged() { + onSetup { + val context1 = activityContext + recreateTaskbar() + val context2 = activityContext + assertThat(context1).isNotSameInstanceAs(context2) + } + } + + @Test + fun testTeardown_taskbarDestroyed() { + val testRule = TaskbarUnitTestRule(this, context) + testRule.apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + assertThrows(RuntimeException::class.java) { testRule.activityContext } + } + + @Test + fun testInjectController_validControllerType_isInjected() { + val testClass = + object { + @InjectController lateinit var controller: TaskbarStashController + val isInjected: Boolean + get() = ::controller.isInitialized + } + + TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.isInjected).isTrue() + } + } + + @Test + fun testInjectController_multipleControllers_areInjected() { + val testClass = + object { + @InjectController lateinit var controller1: TaskbarStashController + @InjectController lateinit var controller2: TaskbarKeyguardController + val areInjected: Boolean + get() = ::controller1.isInitialized && ::controller2.isInitialized + } + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.areInjected).isTrue() + } + } + + @Test + fun testInjectController_invalidControllerType_exceptionThrown() { + val testClass = + object { + @InjectController lateinit var manager: TaskbarManager // Not a controller. + } + + // We cannot use #assertThrows because we also catch an assumption violated exception when + // running #evaluate on devices that do not support Taskbar. + val result = + try { + TaskbarUnitTestRule(testClass, context) + .apply(EMPTY_STATEMENT, DESCRIPTION) + .evaluate() + } catch (e: NoSuchElementException) { + e + } + assertThat(result).isInstanceOf(NoSuchElementException::class.java) + } + + @Test + fun testInjectController_recreateTaskbar_controllerChanged() { + val testClass = + object { + @InjectController lateinit var controller: TaskbarStashController + } + + onSetup(TaskbarUnitTestRule(testClass, context)) { + val controller1 = testClass.controller + recreateTaskbar() + val controller2 = testClass.controller + assertThat(controller1).isNotSameInstanceAs(controller2) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR) + @Test + fun testInjectBubbleController_bubbleFlagOn_isInjected() { + val testClass = + object { + @InjectController lateinit var controller: BubbleBarController + val isInjected: Boolean + get() = ::controller.isInitialized + } + + TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.isInjected).isTrue() + } + } + + @DisableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR) + @Test + fun testInjectBubbleController_bubbleFlagOff_exceptionThrown() { + val testClass = + object { + @InjectController lateinit var controller: BubbleBarController + } + + // We cannot use #assertThrows because we also catch an assumption violated exception when + // running #evaluate on devices that do not support Taskbar. + val result = + try { + TaskbarUnitTestRule(testClass, context) + .apply(EMPTY_STATEMENT, DESCRIPTION) + .evaluate() + } catch (e: NoSuchElementException) { + e + } + assertThat(result).isInstanceOf(NoSuchElementException::class.java) + } + + @Test + fun testUserSetupMode_default_isComplete() { + onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() } + } + + @Test + fun testUserSetupMode_withAnnotation_isIncomplete() { + @UserSetupMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isUserSetupComplete).isFalse() + } + } + + @Test + fun testNavBarKidsMode_default_navBarNotForcedVisible() { + onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() } + } + + @Test + fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() { + @NavBarKidsMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isNavBarForceVisible).isTrue() + } + } + + @Test + fun testForceRtlAnnotation_setsActivityContextLayoutDirection() { + @ForceRtl class Rtl + onSetup(description = Description.createSuiteDescription(Rtl::class.java)) { + assertThat(Utilities.isRtl(activityContext.resources)).isTrue() + } + } + + /** + * Executes [runTest] after the [testRule] setup phase completes. + * + * A [description] can also be provided to mimic annotating a test or test class. + */ + private fun onSetup( + testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context), + description: Description = DESCRIPTION, + runTest: TaskbarUnitTestRule.() -> Unit, + ) { + testRule + .apply( + object : Statement() { + override fun evaluate() = runTest(testRule) + }, + description, + ) + .evaluate() + } + + private companion object { + private val EMPTY_STATEMENT = + object : Statement() { + override fun evaluate() = Unit + } + private val DESCRIPTION = + Description.createSuiteDescription(TaskbarUnitTestRuleTest::class.java) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt new file mode 100644 index 000000000..d96e06eca --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt @@ -0,0 +1,182 @@ +/* + * 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.launcher3.taskbar.rules + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.util.AllModulesMinusWMProxy +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.FakePrefsModule +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.SettingsCacheSandbox +import com.android.launcher3.util.window.WindowManagerProxy +import com.android.quickstep.SystemUiProxy +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import org.junit.rules.ExternalResource +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.spy + +/** + * [SandboxApplication] for running Taskbar tests. + * + * Tests need to run on a [VirtualDisplay] to avoid conflicting with Launcher's Taskbar on the + * [DEFAULT_DISPLAY] (i.e. test is executing on a device). + */ +class TaskbarWindowSandboxContext +private constructor( + private val base: SandboxApplication, + val virtualDisplay: VirtualDisplay, + private val params: SandboxParams, +) : ContextWrapper(base), TestRule { + + val settingsCacheSandbox = SettingsCacheSandbox() + + private val virtualDisplayRule = + object : ExternalResource() { + override fun after() = virtualDisplay.release() + } + + private val singletonSetupRule = + object : ExternalResource() { + override fun before() { + val context = this@TaskbarWindowSandboxContext + val builder = + params.builderBase + .bindSystemUiProxy(params.systemUiProxyProvider.invoke(context)) + .bindSettingsCache(settingsCacheSandbox.cache) + base.initDaggerComponent(builder) + } + } + + override fun apply(statement: Statement, description: Description): Statement { + return RuleChain.outerRule(virtualDisplayRule) + .around(base) + .around(singletonSetupRule) + .apply(statement, description) + } + + companion object { + private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay" + + /** Creates a [SandboxApplication] for Taskbar tests. */ + fun create(params: SandboxParams = SandboxParams()): TaskbarWindowSandboxContext { + val base = ApplicationProvider.getApplicationContext() + val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java)) + // Create virtual display to avoid clashing with Taskbar on default display. + val virtualDisplay = + base.resources.displayMetrics.let { + displayManager.createVirtualDisplay( + VIRTUAL_DISPLAY_NAME, + it.widthPixels, + it.heightPixels, + it.densityDpi, + /* surface= */ null, + /* flags= */ 0, + ) + } + + return TaskbarWindowSandboxContext( + SandboxApplication(base.createDisplayContext(virtualDisplay.display)), + virtualDisplay, + params, + ) + } + } +} + +/** A wrapper over display controller which allows modifying the underlying info */ +@LauncherAppSingleton +class DisplayControllerSpy +@Inject +constructor( + @ApplicationContext context: Context, + wmProxy: WindowManagerProxy, + prefs: LauncherPrefs, + lifecycle: DaggerSingletonTracker, +) : DisplayController(context, wmProxy, prefs, lifecycle) { + + var infoModifier: ((Info) -> Info)? = null + + override fun getInfo(): Info = infoModifier?.invoke(super.getInfo()) ?: super.getInfo() +} + +@Module +abstract class DisplayControllerModule { + @Binds abstract fun bindDisplayController(controller: DisplayControllerSpy): DisplayController +} + +@Module +object DesktopVisibilityControllerModule { + @JvmStatic + @Provides + @LauncherAppSingleton + fun provideDesktopVisibilityController( + @ApplicationContext context: Context, + systemUiProxy: SystemUiProxy, + lifecycleTracker: DaggerSingletonTracker, + ): DesktopVisibilityController { + return spy(DesktopVisibilityController(context, systemUiProxy, lifecycleTracker)) + } +} + +@LauncherAppSingleton +@Component( + modules = + [ + AllModulesMinusWMProxy::class, + FakePrefsModule::class, + DisplayControllerModule::class, + TaskbarSandboxModule::class, + DesktopVisibilityControllerModule::class, + ] +) +interface TaskbarSandboxComponent : LauncherAppComponent { + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder + + @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder + + override fun build(): TaskbarSandboxComponent + } +} + +/** Include additional bindings when building a [TaskbarSandboxComponent]. */ +data class SandboxParams( + val systemUiProxyProvider: (Context) -> SystemUiProxy = { SystemUiProxy(it) }, + val builderBase: TaskbarSandboxComponent.Builder = DaggerTaskbarSandboxComponent.builder(), +) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt new file mode 100644 index 000000000..69095e7e7 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt @@ -0,0 +1,47 @@ +/* + * 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.launcher3.taskbar.rules + +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.model.Statement + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023"]) +class TaskbarWindowSandboxContextTest { + + @Test + fun testVirtualDisplay_releasedOnTeardown() { + val context = TaskbarWindowSandboxContext.create() + assertThat(context.virtualDisplay.token).isNotNull() + + context + .apply( + object : Statement() { + override fun evaluate() = Unit + }, + Description.createSuiteDescription(TaskbarWindowSandboxContextTest::class.java), + ) + .evaluate() + + assertThat(context.virtualDisplay.token).isNull() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt new file mode 100644 index 000000000..52238c83d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt @@ -0,0 +1,61 @@ +/* + * 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.launcher3.util + +import android.net.Uri +import com.android.launcher3.util.SettingsCache.OnChangeListener +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Provides [SettingsCache] sandboxed from system settings for testing. */ +class SettingsCacheSandbox { + private val values = mutableMapOf() + private val listeners = mutableMapOf>() + + /** + * Fake cache that delegates: + * - [SettingsCache.getValue] to [values] + * - [SettingsCache.mListenerMap] to [listeners]. + */ + val cache = + mock { + on { getValue(any()) } doAnswer { mock.getValue(it.getArgument(0), 1) } + on { getValue(any(), any()) } doAnswer + { + values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1 + } + + doAnswer { + listeners.getOrPut(it.getArgument(0)) { mutableSetOf() }.add(it.getArgument(1)) + } + .whenever(mock) + .register(any(), any()) + doAnswer { listeners[it.getArgument(0)]?.remove(it.getArgument(1)) } + .whenever(mock) + .unregister(any(), any()) + } + + operator fun get(key: Uri): Int? = values[key] + + operator fun set(key: Uri, value: Int) { + if (value == values[key]) return + values[key] = value + listeners[key]?.forEach { it.onSettingsChanged(value == 1) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt new file mode 100644 index 000000000..9b0a95a52 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget.picker + +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class WidgetCategoryFilterTest { + + @Test + fun filterValueZero_everythingMatches() { + val noFilter = WidgetCategoryFilter(categoryMask = 0) + + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + noFilter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_NOT_KEYGUARD) + noFilter.assertMatches( + WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD + ) + noFilter.assertMatches( + WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_NOT_KEYGUARD + ) + noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + } + + @Test + fun includeHomeScreen_matchesOnlyIfHomeScreenExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX) + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX) + } + + @Test + fun includeHomeScreenOrKeyguard_matchesIfEitherHomeScreenOrKeyguardExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + } + + @Test + fun excludeNotKeyguard_doesNotMatchIfNotKeyguardExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_NOT_KEYGUARD.inv()) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch( + WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN + ) + } + + @Test + fun multipleExclusions_doesNotMatchIfExcludedCategoriesExist() { + val filter = + WidgetCategoryFilter( + WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv() + ) + + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch( + WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN + ) + } + + private fun WidgetCategoryFilter.assertMatches(category: Int) { + assertThat(matches(category)).isTrue() + } + + private fun WidgetCategoryFilter.assertDoesNotMatch(category: Int) { + assertThat(matches(category)).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java new file mode 100644 index 000000000..6adb7b405 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java @@ -0,0 +1,402 @@ +/* + * 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.quickstep; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; +import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.ViewTreeObserver; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherRootView; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StatefulContainer; +import com.android.launcher3.util.LauncherModelHelper; +import com.android.launcher3.util.MSDLPlayerWrapper; +import com.android.launcher3.util.SystemUiController; +import com.android.quickstep.util.ContextInitListener; +import com.android.quickstep.util.MotionPauseDetector; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.Flags; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.wm.shell.shared.split.SplitBounds; + +import com.google.android.msdl.data.model.MSDLToken; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Collections; +import java.util.HashMap; + +public abstract class AbsSwipeUpHandlerTestCase< + STATE_TYPE extends BaseState, + RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer, + RECENTS_VIEW extends RecentsView, + SWIPE_HANDLER extends AbsSwipeUpHandler, + CONTAINER_INTERFACE extends BaseContainerInterface> { + + protected final LauncherModelHelper mLauncherModelHelper = new LauncherModelHelper(); + protected final LauncherModelHelper.SandboxModelContext mContext = + mLauncherModelHelper.sandboxContext; + protected final InputConsumerController mInputConsumerController = + InputConsumerController.getRecentsAnimationInputConsumer(); + protected final ActivityManager.RunningTaskInfo mRunningTaskInfo = + new ActivityManager.RunningTaskInfo(); + protected final TopTaskTracker.CachedTaskInfo mCachedTaskInfo = + new TopTaskTracker.CachedTaskInfo( + Collections.singletonList(mRunningTaskInfo), /* canEnterDesktop = */ false, + DEFAULT_DISPLAY); + protected final RemoteAnimationTarget mRemoteAnimationTarget = new RemoteAnimationTarget( + /* taskId= */ 0, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ mRunningTaskInfo, + /* allowEnterPip= */ false); + + protected RecentsAnimationTargets mRecentsAnimationTargets; + protected TaskAnimationManager mTaskAnimationManager; + + @Mock protected CONTAINER_INTERFACE mActivityInterface; + @Mock protected ContextInitListener mContextInitListener; + @Mock protected RecentsAnimationController mRecentsAnimationController; + @Mock protected STATE_TYPE mState; + @Mock protected ViewTreeObserver mViewTreeObserver; + @Mock protected DragLayer mDragLayer; + @Mock protected LauncherRootView mRootView; + @Mock protected SystemUiController mSystemUiController; + @Mock protected GestureState mGestureState; + @Mock protected MSDLPlayerWrapper mMSDLPlayerWrapper; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUpAnimationTargets() { + Bundle extras = new Bundle(); + extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true); + extras.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SNAP_TO_2_50_50)); + mRecentsAnimationTargets = new RecentsAnimationTargets( + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + /* homeContentInsets= */ new Rect(), + /* minimizedHomeBounds= */ null, + extras); + } + + @Before + public void setUpRunningTaskInfo() { + mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Before + public void setUpGestureState() { + when(mGestureState.getRunningTask()).thenReturn(mCachedTaskInfo); + when(mGestureState.getLastAppearedTaskIds()).thenReturn(new int[0]); + when(mGestureState.getLastStartedTaskIds()).thenReturn(new int[1]); + when(mGestureState.getHomeIntent()).thenReturn(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + doReturn(mActivityInterface).when(mGestureState).getContainerInterface(); + } + + @Before + public void setUpRecentsView() { + RECENTS_VIEW recentsView = getRecentsView(); + when(recentsView.getViewTreeObserver()).thenReturn(mViewTreeObserver); + doAnswer(answer -> { + runOnMainSync(() -> answer.getArgument(0).run()); + return this; + }).when(recentsView).runOnPageScrollsInitialized(any()); + } + + @Before + public void setUpRecentsContainer() { + mTaskAnimationManager = new TaskAnimationManager(mContext, + RecentsAnimationDeviceState.INSTANCE.get(mContext), DEFAULT_DISPLAY); + RecentsViewContainer recentsContainer = getRecentsContainer(); + RECENTS_VIEW recentsView = getRecentsView(); + + when(recentsContainer.getDeviceProfile()).thenReturn(new DeviceProfile()); + when(recentsContainer.getOverviewPanel()).thenReturn(recentsView); + when(recentsContainer.getDragLayer()).thenReturn(mDragLayer); + when(recentsContainer.getRootView()).thenReturn(mRootView); + when(recentsContainer.getSystemUiController()).thenReturn(mSystemUiController); + when(mActivityInterface.createActivityInitListener(any())) + .thenReturn(mContextInitListener); + doReturn(recentsContainer).when(mActivityInterface).getCreatedContainer(); + doAnswer(answer -> { + answer.getArgument(0).run(); + return this; + }).when(recentsContainer).runOnBindToTouchInteractionService(any()); + } + + @Test + public void testInitWhenReady_registersActivityInitListener() { + String reasonString = "because i said so"; + + createSwipeHandler().initWhenReady(reasonString); + verify(mContextInitListener).register(eq(reasonString)); + } + + @Test + public void testOnRecentsAnimationCanceled_unregistersActivityInitListener() { + createSwipeHandler() + .onRecentsAnimationCanceled(new HashMap<>()); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled"))); + } + + @Test + public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() { + createSwipeHandler().onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister("AbsSwipeUpHandler.invalidateHandler")); + } + + @Test + public void testOnConsumerAboutToBeSwitched_midQuickSwitch_unregistersActivityInitListener() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK) + .onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation"))); + } + + @Test + public void testStartNewTask_finishesRecentsAnimationController() { + SWIPE_HANDLER absSwipeUpHandler = createSwipeHandler(); + + onRecentsAnimationStart(absSwipeUpHandler); + + runOnMainSync(() -> { + absSwipeUpHandler.startNewTask(unused -> {}); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @Test + public void testHomeGesture_finishesRecentsAnimationController() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).detachNavigationBarFromApp(true); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_handsOffAnimation() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController, never()).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @Test + public void testHomeGesture_invalidatesHandlerAfterParallelAnim() { + ValueAnimator parallelAnim = new ValueAnimator(); + parallelAnim.setRepeatCount(ValueAnimator.INFINITE); + when(mActivityInterface.getParallelAnimationToGestureEndTarget(any(), anyLong(), any())) + .thenReturn(parallelAnim); + SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + runOnMainSync(() -> { + parallelAnim.start(); + verifyRecentsAnimationFinishedAndCallCallback(); + assertFalse(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + parallelAnim.end(); + assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + }); + } + + @Test + public void testHomeGesture_invalidatesHandlerIfNoParallelAnim() { + when(mActivityInterface.getParallelAnimationToGestureEndTarget(any(), anyLong(), any())) + .thenReturn(null); + SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + runOnMainSync(() -> { + verifyRecentsAnimationFinishedAndCallCallback(); + assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + }); + } + + @Test + @EnableFlags(com.android.launcher3.Flags.FLAG_MSDL_FEEDBACK) + public void onMotionPauseDetected_playsSwipeThresholdToken() { + SWIPE_HANDLER handler = createSwipeHandler(); + MotionPauseDetector.OnMotionPauseListener listener = handler.getMotionPauseListener(); + listener.onMotionPauseDetected(); + + verify(mMSDLPlayerWrapper, times(1)).playToken(eq(MSDLToken.SWIPE_THRESHOLD_INDICATOR)); + verifyNoMoreInteractions(mMSDLPlayerWrapper); + } + + /** + * Verifies that RecentsAnimationController#finish() is called, and captures and runs any + * callback that was passed to it. This ensures that STATE_CURRENT_TASK_FINISHED is correctly + * set for example. + */ + private void verifyRecentsAnimationFinishedAndCallCallback() { + ArgumentCaptor finishCallback = ArgumentCaptor.forClass(Runnable.class); + // Check if the 2 parameter method is called. + verify(mRecentsAnimationController, atLeast(0)).finish( + anyBoolean(), finishCallback.capture()); + if (finishCallback.getAllValues().isEmpty()) { + // Check if the 3 parameter method is called. + verify(mRecentsAnimationController).finish( + anyBoolean(), finishCallback.capture(), anyBoolean()); + } + if (finishCallback.getValue() != null) { + finishCallback.getValue().run(); + } + } + + private SWIPE_HANDLER createSwipeUpHandlerForGesture(GestureState.GestureEndTarget endTarget) { + boolean isQuickSwitch = endTarget == GestureState.GestureEndTarget.NEW_TASK; + + doReturn(mState).when(mActivityInterface).stateFromGestureEndTarget(any()); + + SWIPE_HANDLER swipeHandler = createSwipeHandler(SystemClock.uptimeMillis(), isQuickSwitch); + + swipeHandler.onActivityInit(/* alreadyOnHome= */ false); + swipeHandler.onGestureStarted(isQuickSwitch); + onRecentsAnimationStart(swipeHandler); + + when(mGestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]); + runOnMainSync(swipeHandler::switchToScreenshot); + + when(mGestureState.getEndTarget()).thenReturn(endTarget); + when(mGestureState.isRecentsAnimationRunning()).thenReturn(isQuickSwitch); + float xVelocityPxPerMs = isQuickSwitch ? 100 : 0; + float yVelocityPxPerMs = isQuickSwitch ? 0 : -100; + swipeHandler.onGestureEnded( + yVelocityPxPerMs, new PointF(xVelocityPxPerMs, yVelocityPxPerMs), isQuickSwitch); + swipeHandler.onCalculateEndTarget(); + runOnMainSync(swipeHandler::onSettledOnEndTarget); + + return swipeHandler; + } + + private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) { + runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart( + mRecentsAnimationController, mRecentsAnimationTargets, /* transitionInfo= */null)); + } + + protected static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + @NonNull + private SWIPE_HANDLER createSwipeHandler() { + return createSwipeHandler(SystemClock.uptimeMillis(), false); + } + + @NonNull + protected abstract SWIPE_HANDLER createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture); + + @NonNull + protected abstract RecentsViewContainer getRecentsContainer(); + + @NonNull + protected abstract RECENTS_VIEW getRecentsView(); +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt index 73b35e8a5..a1bd107ca 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt @@ -18,32 +18,59 @@ package com.android.quickstep import android.app.PendingIntent import android.content.IIntentSender +import android.provider.Settings +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.AllModulesForTest import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.SettingsCacheSandbox import com.android.launcher3.util.TestUtil import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit.SECONDS +import org.junit.After +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith private const val TIMEOUT = 5L +private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE) @RunWith(AndroidJUnit4::class) class AllAppsActionManagerTest { private val callbackSemaphore = Semaphore(0) private val bgExecutor = UI_HELPER_EXECUTOR - private val allAppsActionManager = - AllAppsActionManager( - InstrumentationRegistry.getInstrumentation().targetContext, - bgExecutor, - ) { - callbackSemaphore.release() - PendingIntent(IIntentSender.Default()) + @get:Rule val context = SandboxApplication() + + private val settingsCacheSandbox = + SettingsCacheSandbox().also { it[USER_SETUP_COMPLETE_URI] = 1 } + + private val allAppsActionManager by + lazy(LazyThreadSafetyMode.NONE) { + AllAppsActionManager(context, bgExecutor) { + callbackSemaphore.release() + PendingIntent(IIntentSender.Default()) + } } + @Before + fun initDaggerComponent() { + context.initDaggerComponent( + DaggerAllAppsActionManagerTestComponent.builder() + .bindSettingsCache(settingsCacheSandbox.cache) + ) + } + + @After fun destroyManager() = allAppsActionManager.onDestroy() + @Test fun taskbarPresent_actionRegistered() { allAppsActionManager.isTaskbarPresent = true @@ -88,4 +115,50 @@ class AllAppsActionManagerTest { assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() assertThat(allAppsActionManager.isActionRegistered).isTrue() } + + @Test + fun taskbarPresent_userSetupIncomplete_actionUnregistered() { + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0 + allAppsActionManager.isTaskbarPresent = true + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun taskbarPresent_setupUiVisible_actionUnregistered() { + allAppsActionManager.isSetupUiVisible = true + allAppsActionManager.isTaskbarPresent = true + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun taskbarPresent_userSetupCompleted_actionRegistered() { + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0 + allAppsActionManager.isTaskbarPresent = true + + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 1 + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + } + + @Test + fun taskbarPresent_setupUiDismissed_actionRegistered() { + allAppsActionManager.isSetupUiVisible = true + allAppsActionManager.isTaskbarPresent = true + + allAppsActionManager.isSetupUiVisible = false + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + } +} + +@LauncherAppSingleton +@Component(modules = [AllModulesForTest::class]) +interface AllAppsActionManagerTestComponent : LauncherAppComponent { + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder + + override fun build(): AllAppsActionManagerTestComponent + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt new file mode 100644 index 000000000..e62455fd0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** Test for [DesktopFullscreenDrawParams] class. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopFullscreenDrawParamsTest() { + private val params = + DesktopFullscreenDrawParams(mock(), cornerRadiusProvider = { CORNER_RADIUS }) + + @Test + fun setMiddleProgress_invariantCornerRadiusForDesktop() { + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + + params.setProgress(fullscreenProgress = 0.67f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + } + + companion object { + const val CORNER_RADIUS = 32f + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt new file mode 100644 index 000000000..fa7907fa4 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt @@ -0,0 +1,70 @@ +/* + * 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.quickstep + +import android.content.Context +import android.view.Display +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import java.io.PrintWriter +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisplayModelTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + class TestableResource : DisplayModel.DisplayResource() { + var isCleanupCalled = false + + override fun cleanup() { + isCleanupCalled = true + } + + override fun dump(prefix: String, writer: PrintWriter) { + // No-Op + } + } + + private val testableDisplayModel = + object : DisplayModel(context) { + override fun createDisplayResource(display: Display): TestableResource { + return TestableResource() + } + } + + @Test + fun testCreate() { + testableDisplayModel.storeDisplayResource(Display.DEFAULT_DISPLAY) + val resource = testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY) + assertNotNull(resource) + } + + @Test + fun testCleanAndDelete() { + testableDisplayModel.storeDisplayResource(Display.DEFAULT_DISPLAY) + val resource = testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY)!! + assertNotNull(resource) + testableDisplayModel.deleteDisplayResource(Display.DEFAULT_DISPLAY) + assert(resource.isCleanupCalled) + assertNull(testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY)) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java new file mode 100644 index 000000000..3489519e7 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java @@ -0,0 +1,66 @@ +/* + * 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.quickstep; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsState; + +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase< + RecentsState, + RecentsActivity, + FallbackRecentsView, + FallbackSwipeHandler, + FallbackActivityInterface> { + + @Mock private RecentsActivity mRecentsActivity; + @Mock private FallbackRecentsView mRecentsView; + + + @Override + protected FallbackSwipeHandler createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new FallbackSwipeHandler( + mContext, + mTaskAnimationManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected RecentsActivity getRecentsContainer() { + return mRecentsActivity; + } + + @NonNull + @Override + protected FallbackRecentsView getRecentsView() { + return mRecentsView; + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index 5d62a4c74..99b81e05e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -20,21 +20,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.launcher3.FakeInvariantDeviceProfileTest import com.android.quickstep.util.TaskCornerRadius -import com.android.quickstep.views.TaskView.FullscreenDrawParams import com.android.systemui.shared.system.QuickStepContract import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy +import org.mockito.kotlin.mock -/** Test for FullscreenDrawParams class. */ +/** Test for [FullscreenDrawParams] class. */ @SmallTest @RunWith(AndroidJUnit4::class) class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { - private lateinit var params: FullscreenDrawParams @Before @@ -46,115 +42,108 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { fun setStartProgress_correctCornerRadiusForTablet() { initializeVarsForTablet() - params.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 0f, parentScale = 1.0f, taskViewScale = 1.0f) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setFullProgress_correctCornerRadiusForTablet() { initializeVarsForTablet() - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setStartProgress_correctCornerRadiusForPhone() { initializeVarsForPhone() - params.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setFullProgress_correctCornerRadiusForPhone() { initializeVarsForPhone() - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setStartProgress_correctCornerRadiusForMultiDisplay() { - val display1Context = context - val display2Context = mock(Context::class.java) - val spyParams = spy(params) + val display1Context = mock() + val display2Context = mock() + val display1TaskRadius = TASK_CORNER_RADIUS + 1 + val display2TaskRadius = TASK_CORNER_RADIUS + 2 - val display1TaskRadius = TaskCornerRadius.get(display1Context) - val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context) - val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different. - val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different. - doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context) - doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context) + val params = + FullscreenDrawParams( + context, + taskCornerRadiusProvider = { context -> + when (context) { + display1Context -> display1TaskRadius + display2Context -> display2TaskRadius + else -> TASK_CORNER_RADIUS + } + }, + windowCornerRadiusProvider = { 0f }, + ) - spyParams.updateCornerRadius(display1Context) - spyParams.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1TaskRadius) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(TASK_CORNER_RADIUS) - spyParams.updateCornerRadius(display2Context) - spyParams.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2TaskRadius) + params.updateCornerRadius(display1Context) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display1TaskRadius) + + params.updateCornerRadius(display2Context) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display2TaskRadius) } @Test fun setFullProgress_correctCornerRadiusForMultiDisplay() { - val display1Context = context - val display2Context = mock(Context::class.java) - val spyParams = spy(params) + val display1Context = mock() + val display2Context = mock() + val display1WindowRadius = WINDOW_CORNER_RADIUS + 1 + val display2WindowRadius = WINDOW_CORNER_RADIUS + 2 - val display1TaskRadius = TaskCornerRadius.get(display1Context) - val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context) - val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different. - val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different. - doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context) - doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context) + val params = + FullscreenDrawParams( + context, + taskCornerRadiusProvider = { 0f }, + windowCornerRadiusProvider = { context -> + when (context) { + display1Context -> display1WindowRadius + display2Context -> display2WindowRadius + else -> WINDOW_CORNER_RADIUS + } + }, + ) - spyParams.updateCornerRadius(display1Context) - spyParams.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1WindowRadius) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(WINDOW_CORNER_RADIUS) - spyParams.updateCornerRadius(display2Context) - spyParams.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f, - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2WindowRadius) + params.updateCornerRadius(display1Context) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display1WindowRadius) + + params.updateCornerRadius(display2Context) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display2WindowRadius) + } + + companion object { + const val TASK_CORNER_RADIUS = 56f + const val WINDOW_CORNER_RADIUS = 32f } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt new file mode 100644 index 000000000..24f969643 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt @@ -0,0 +1,139 @@ +package com.android.quickstep + +/* + * 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. + */ + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.util.LauncherModelHelper +import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED) +class LauncherRestoreEventLoggerImplTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val mLauncherModelHelper = LauncherModelHelper() + private val mSandboxContext: SandboxModelContext = mLauncherModelHelper.sandboxContext + private lateinit var loggerUnderTest: LauncherRestoreEventLoggerImpl + + @Before + fun setup() { + loggerUnderTest = LauncherRestoreEventLoggerImpl(mSandboxContext) + } + + @After + fun teardown() { + loggerUnderTest.restoreEventLogger.clearData() + mLauncherModelHelper.destroy() + } + + @Test + fun `logLauncherItemsRestoreFailed logs multiple items as failing restore`() { + // Given + val expectedDataType = "application" + val expectedError = "test_failure" + // When + loggerUnderTest.logLauncherItemsRestoreFailed( + dataType = expectedDataType, + count = 5, + error = expectedError + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(5) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } + + @Test + fun `logLauncherItemsRestored logs multiple items as restored`() { + // Given + val expectedDataType = "application" + // When + loggerUnderTest.logLauncherItemsRestored(dataType = expectedDataType, count = 5) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(5) + assertThat(actualResult.failCount).isEqualTo(0) + assertThat(actualResult.errors.keys).isEmpty() + } + + @Test + fun `logSingleFavoritesItemRestored logs a single Favorites Item as restored`() { + // Given + val expectedDataType = "widget" + // When + loggerUnderTest.logSingleFavoritesItemRestored(favoritesId = Favorites.ITEM_TYPE_APPWIDGET) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(1) + assertThat(actualResult.failCount).isEqualTo(0) + assertThat(actualResult.errors.keys).isEmpty() + } + + @Test + fun `logSingleFavoritesItemRestoreFailed logs a single Favorites Item as failing restore`() { + // Given + val expectedDataType = "widget" + val expectedError = "test_failure" + // When + loggerUnderTest.logSingleFavoritesItemRestoreFailed( + favoritesId = Favorites.ITEM_TYPE_APPWIDGET, + error = expectedError + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(1) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } + + @Test + fun `logFavoritesItemsRestoreFailed logs multiple Favorites Items as failing restore`() { + // Given + val expectedDataType = "deep_shortcut" + val expectedError = "test_failure" + // When + loggerUnderTest.logFavoritesItemsRestoreFailed( + favoritesId = Favorites.ITEM_TYPE_DEEP_SHORTCUT, + count = 5, + error = expectedError + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(5) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt new file mode 100644 index 000000000..5661dcf69 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt @@ -0,0 +1,145 @@ +/* + * 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.quickstep + +import android.graphics.PointF +import android.hardware.display.DisplayManager +import android.hardware.display.DisplayManagerGlobal +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY +import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS +import android.view.DisplayInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.R +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppModule +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.LauncherModelHelper +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.shared.system.InputConsumerController +import dagger.BindsInstance +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LauncherSwipeHandlerV2Test { + + @Mock private lateinit var taskAnimationManager: TaskAnimationManager + + private lateinit var gestureState: GestureState + @Mock private lateinit var inputConsumerController: InputConsumerController + + @Mock private lateinit var systemUiProxy: SystemUiProxy + + @Mock private lateinit var msdlPlayerWrapper: MSDLPlayerWrapper + + private lateinit var underTest: LauncherSwipeHandlerV2 + + @get:Rule val mockitoRule = MockitoJUnit.rule() + + private val launcherModelHelper = LauncherModelHelper() + private val sandboxContext = launcherModelHelper.sandboxContext + + private val flingSpeed = + -(sandboxContext.resources.getDimension(R.dimen.quickstep_fling_threshold_speed) + 1) + + private val displayManager: DisplayManager = + sandboxContext.spyService(DisplayManager::class.java) + + @Before + fun setup() { + val display = + Display( + DisplayManagerGlobal.getInstance(), + DEFAULT_DISPLAY, + DisplayInfo(), + DEFAULT_DISPLAY_ADJUSTMENTS, + ) + whenever(displayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(display) + whenever(displayManager.displays).thenReturn(arrayOf(display)) + + sandboxContext.initDaggerComponent( + DaggerTestComponent.builder() + .bindSystemUiProxy(systemUiProxy) + .bindRotationHelper(mock(RotationTouchHelper::class.java)) + .bindRecentsState(mock(RecentsAnimationDeviceState::class.java)) + ) + gestureState = + spy( + GestureState( + OverviewComponentObserver.INSTANCE.get(sandboxContext), + DEFAULT_DISPLAY, + 0, + ) + ) + + underTest = + LauncherSwipeHandlerV2( + sandboxContext, + taskAnimationManager, + gestureState, + 0, + false, + inputConsumerController, + msdlPlayerWrapper, + ) + underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false) + } + + @Test + fun goHomeFromAppByTrackpad_updateEduStats() { + gestureState.setTrackpadGestureType(GestureState.TrackpadGestureType.THREE_FINGER) + underTest.onGestureEnded(flingSpeed, PointF(), /* horizontalTouchSlopPassed= */ false) + verify(systemUiProxy) + .updateContextualEduStats(/* isTrackpadGesture= */ eq(true), eq(GestureType.HOME)) + } + + @Test + fun goHomeFromAppByTouch_updateEduStats() { + underTest.onGestureEnded(flingSpeed, PointF(), /* horizontalTouchSlopPassed= */ false) + verify(systemUiProxy) + .updateContextualEduStats(/* isTrackpadGesture= */ eq(false), eq(GestureType.HOME)) + } +} + +@LauncherAppSingleton +@Component(modules = [LauncherAppModule::class]) +interface TestComponent : LauncherAppComponent { + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder + + @BindsInstance fun bindRotationHelper(helper: RotationTouchHelper): Builder + + @BindsInstance fun bindRecentsState(state: RecentsAnimationDeviceState): Builder + + override fun build(): TestComponent + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java new file mode 100644 index 000000000..66c4ab5ce --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java @@ -0,0 +1,97 @@ +/* + * 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.quickstep; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.Hotseat; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.WorkspaceStateTransitionAnimation; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.views.RecentsView; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +@Ignore +public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase< + LauncherState, + QuickstepLauncher, + RecentsView, + LauncherSwipeHandlerV2, + LauncherActivityInterface> { + + @Mock private QuickstepLauncher mQuickstepLauncher; + @Mock private RecentsView mRecentsView; + @Mock private Workspace mWorkspace; + @Mock private Hotseat mHotseat; + @Mock private WorkspaceStateTransitionAnimation mTransitionAnimation; + + @Before + public void setUpQuickStepLauncher() { + when(mQuickstepLauncher.createAtomicAnimationFactory()) + .thenReturn(new AtomicAnimationFactory<>(0)); + when(mQuickstepLauncher.getHotseat()).thenReturn(mHotseat); + doReturn(mWorkspace).when(mQuickstepLauncher).getWorkspace(); + doReturn(new StateManager(mQuickstepLauncher, LauncherState.NORMAL)) + .when(mQuickstepLauncher).getStateManager(); + + } + + @Before + public void setUpWorkspace() { + when(mWorkspace.getStateTransitionAnimation()).thenReturn(mTransitionAnimation); + } + + @NonNull + @Override + protected LauncherSwipeHandlerV2 createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new LauncherSwipeHandlerV2( + mContext, + mTaskAnimationManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected QuickstepLauncher getRecentsContainer() { + return mQuickstepLauncher; + } + + @NonNull + @Override + protected RecentsView getRecentsView() { + return mRecentsView; + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java new file mode 100644 index 000000000..0ff142a47 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java @@ -0,0 +1,271 @@ +/* + * 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.quickstep; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.util.LauncherMultivalentJUnit; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class MultiStateCallbackTest { + + private int mFlagCount = 0; + private int getNextStateFlag() { + int index = 1 << mFlagCount; + mFlagCount++; + return index; + } + + private final MultiStateCallback mMultiStateCallback = new MultiStateCallback(new String[0]); + private final Runnable mCallback = spy(new Runnable() { + @Override + public void run() {} + }); + private final Consumer mListener = spy(new Consumer() { + @Override + public void accept(Boolean isOn) {} + }); + + @Test + public void testSetState_trackedProperly() { + int watchedAnime = getNextStateFlag(); + + assertThat(mMultiStateCallback.getState()).isEqualTo(0); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isFalse(); + + mMultiStateCallback.setState(watchedAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue(); + } + + @Test + public void testSetState_withMultipleStates_trackedProperly() { + int watchedAnime = getNextStateFlag(); + int sharedMemes = getNextStateFlag(); + + mMultiStateCallback.setState(watchedAnime); + mMultiStateCallback.setState(sharedMemes); + + assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime | sharedMemes); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue(); + assertThat(mMultiStateCallback.hasStates(sharedMemes)).isTrue(); + assertThat(mMultiStateCallback.hasStates(watchedAnime | sharedMemes)).isTrue(); + } + + @Test + public void testClearState_trackedProperly() { + int lovedAnime = getNextStateFlag(); + + mMultiStateCallback.setState(lovedAnime); + mMultiStateCallback.clearState(lovedAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(0); + assertThat(mMultiStateCallback.hasStates(lovedAnime)).isFalse(); + } + + @Test + public void testClearState_withMultipleState_trackedProperly() { + int lovedAnime = getNextStateFlag(); + int talkedAboutAnime = getNextStateFlag(); + + mMultiStateCallback.setState(lovedAnime); + mMultiStateCallback.setState(talkedAboutAnime); + mMultiStateCallback.clearState(talkedAboutAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(lovedAnime); + assertThat(mMultiStateCallback.hasStates(lovedAnime)).isTrue(); + assertThat(mMultiStateCallback.hasStates(talkedAboutAnime)).isFalse(); + assertThat(mMultiStateCallback.hasStates(lovedAnime | talkedAboutAnime)).isFalse(); + } + + @Test + public void testCallbackDoesNotRun_withoutState() { + int watchedOnePiece = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedOnePiece, mCallback); + + verify(mCallback, never()).run(); + } + + @Test + public void testCallbackDoesNotRun_whenNotTracked() { + int watchedJujutsuKaisen = getNextStateFlag(); + + mMultiStateCallback.setState(watchedJujutsuKaisen); + + verify(mCallback, never()).run(); + } + + @Test + public void testCallbackRuns_afterTrackedAndStateSet() { + int watchedHunterXHunter = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback); + mMultiStateCallback.setState(watchedHunterXHunter); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onUiThread() { + int watchedHunterXHunter = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback); + mMultiStateCallback.setStateOnUiThread(watchedHunterXHunter); + + runOnMainSync(() -> verify(mCallback, times(1)).run()); + } + + @Test + public void testCallbackRuns_agnosticallyToCallOrder() { + int watchedFullMetalAlchemist = getNextStateFlag(); + + mMultiStateCallback.setState(watchedFullMetalAlchemist); + mMultiStateCallback.runOnceAtState(watchedFullMetalAlchemist, mCallback); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onlyOnceAfterStateSet() { + int watchedBleach = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedBleach, mCallback); + mMultiStateCallback.setState(watchedBleach); + mMultiStateCallback.setState(watchedBleach); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onlyOnceAfterClearState() { + int rememberedGreatShow = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(rememberedGreatShow, mCallback); + mMultiStateCallback.setState(rememberedGreatShow); + mMultiStateCallback.clearState(rememberedGreatShow); + mMultiStateCallback.setState(rememberedGreatShow); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackDoesNotRun_withoutFullStateSet() { + int watchedMobPsycho = getNextStateFlag(); + int watchedVinlandSaga = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedMobPsycho | watchedVinlandSaga, mCallback); + mMultiStateCallback.setState(watchedMobPsycho); + + verify(mCallback, times(0)).run(); + } + + @Test + public void testCallbackRuns_withFullStateSet_agnosticallyToCallOrder() { + int watchedReZero = getNextStateFlag(); + int watchedJojosBizareAdventure = getNextStateFlag(); + + mMultiStateCallback.setState(watchedJojosBizareAdventure); + mMultiStateCallback.runOnceAtState(watchedReZero | watchedJojosBizareAdventure, mCallback); + mMultiStateCallback.setState(watchedReZero); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_withFullStateSet_asIntegerMask() { + int watchedPokemon = getNextStateFlag(); + int watchedDigimon = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedPokemon | watchedDigimon, mCallback); + mMultiStateCallback.setState(watchedPokemon | watchedDigimon); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackDoesNotRun_afterClearState() { + int watchedMonster = getNextStateFlag(); + int watchedPingPong = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedMonster | watchedPingPong, mCallback); + mMultiStateCallback.setState(watchedMonster); + mMultiStateCallback.clearState(watchedMonster); + mMultiStateCallback.setState(watchedPingPong); + + verify(mCallback, times(0)).run(); + } + + @Test + public void testlistenerRuns_multipleTimes() { + int watchedSteinsGate = getNextStateFlag(); + + mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener); + mMultiStateCallback.setState(watchedSteinsGate); + + // Called exactly one + verify(mListener, times(1)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + // Never called with isOn = false + verify(mListener, times(0)).accept(eq(false)); + + mMultiStateCallback.clearState(watchedSteinsGate); + + // Called exactly twice + verify(mListener, times(2)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + // Called exactly once with isOn = false + verify(mListener, times(1)).accept(eq(false)); + } + + @Test + public void testlistenerDoesNotRun_forUnchangedState() { + int watchedSteinsGate = getNextStateFlag(); + + mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener); + mMultiStateCallback.setState(watchedSteinsGate); + mMultiStateCallback.setState(watchedSteinsGate); + + // State remained unchanged + verify(mListener, times(1)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + } + + private static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt new file mode 100644 index 000000000..11e0ee88b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -0,0 +1,259 @@ +/* + * 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.quickstep + +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.TestDispatcherProvider +import com.android.launcher3.util.rule.setFlags +import com.android.quickstep.OverviewCommandHelper.CommandInfo +import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus +import com.android.quickstep.OverviewCommandHelper.CommandType +import com.android.quickstep.fallback.window.RecentsDisplayModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(LauncherMultivalentJUnit::class) +@OptIn(ExperimentalCoroutinesApi::class) +class OverviewCommandHelperTest { + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + + private lateinit var sut: OverviewCommandHelper + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private var pendingCallbacksWithDelays = mutableListOf() + + private val recentsDisplayModel: RecentsDisplayModel = mock() + private val defaultDisplayResource: RecentsDisplayModel.RecentsDisplayResource = mock() + private val secondaryDisplayResource: RecentsDisplayModel.RecentsDisplayResource = mock() + private val executeCommandDisplayIds = mutableListOf() + + private fun setupDefaultDisplay() { + whenever(defaultDisplayResource.displayId).thenReturn(DEFAULT_DISPLAY) + whenever(recentsDisplayModel.activeDisplayResources) + .thenReturn(listOf(defaultDisplayResource)) + } + + private fun setupMultipleDisplays() { + whenever(defaultDisplayResource.displayId).thenReturn(DEFAULT_DISPLAY) + whenever(secondaryDisplayResource.displayId).thenReturn(1) + whenever(recentsDisplayModel.activeDisplayResources) + .thenReturn(listOf(defaultDisplayResource, secondaryDisplayResource)) + } + + @Suppress("UNCHECKED_CAST") + @Before + fun setup() { + setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT) + + setupDefaultDisplay() + + sut = + spy( + OverviewCommandHelper( + touchInteractionService = mock(), + overviewComponentObserver = mock(), + dispatcherProvider = TestDispatcherProvider(dispatcher), + recentsDisplayModel = recentsDisplayModel, + focusState = mock(), + taskbarManager = mock(), + ) + ) + + doAnswer { invocation -> + val pendingCallback = invocation.arguments[1] as () -> Unit + + val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull() + if (delayInMillis != null) { + runBlocking { + testScope.backgroundScope.launch { + delay(delayInMillis) + pendingCallback.invoke() + } + } + } + val commandInfo = invocation.arguments[0] as CommandInfo + executeCommandDisplayIds.add(commandInfo.displayId) + delayInMillis == null // if no callback to execute, returns success + } + .`when`(sut) + .executeCommand(any(), any()) + } + + private fun addCallbackDelay(delayInMillis: Long = 0) { + pendingCallbacksWithDelays.add(delayInMillis) + } + + @Test + fun whenFirstCommandIsAdded_executeCommandImmediately() = + testScope.runTest { + // Add command to queue + val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() = + testScope.runTest { + addCallbackDelay(100) + + // Add command to queue + val commandType = CommandType.HOME + val commandInfo: CommandInfo = sut.addCommand(commandType)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(200L) + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenFirstCommandIsPendingCallback_NextCommandWillWait() = + testScope.runTest { + // Add command to queue + addCallbackDelay(100) + val commandType1 = CommandType.HOME + val commandInfo1: CommandInfo = sut.addCommand(commandType1)!! + assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) + + addCallbackDelay(100) + val commandType2 = CommandType.SHOW + val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + advanceTimeBy(101L) + assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(101L) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() = + testScope.runTest { + // Add command to queue + addCallbackDelay(QUEUE_TIMEOUT) + val commandType1 = CommandType.HOME + val commandInfo1: CommandInfo = sut.addCommand(commandType1)!! + assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) + + addCallbackDelay(100) + val commandType2 = CommandType.SHOW + val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + advanceTimeBy(QUEUE_TIMEOUT) + assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(101) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenAllDisplaysCommandIsAdded_singleCommandProcessedForDefaultDisplay() = + testScope.runTest { + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(DEFAULT_DISPLAY) + } + + @Test + fun whenAllDisplaysCommandIsAdded_multipleCommandsProcessedForMultipleDisplays() = + testScope.runTest { + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds) + .containsExactly(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenAllExceptDisplayCommandIsAdded_otherDisplayProcessed() = + testScope.runTest { + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = + sut.addCommandsForDisplaysExcept(CommandType.HOME, DEFAULT_DISPLAY)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenSingleDisplayCommandIsAdded_thatDisplayIsProcessed() = + testScope.runTest { + executeCommandDisplayIds.clear() + val displayId = 5 + // Add command to queue + val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME, displayId)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(displayId) + } + + private companion object { + const val QUEUE_TIMEOUT = 5001L + const val EXTERNAL_DISPLAY_ID = 1 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java new file mode 100644 index 000000000..1e4315a04 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.launcher3.Flags.FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS; +import static com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.TestCase.assertNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManager.RecentTaskInfo; +import android.app.KeyguardManager; +import android.app.TaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.launcher3.statehandlers.DesktopVisibilityController; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.LooperExecutor; +import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.TaskViewType; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedTaskInfo; +import com.android.wm.shell.shared.split.SplitBounds; +import com.android.wm.shell.shared.split.SplitScreenConstants; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RecentTasksListTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private SystemUiProxy mSystemUiProxy; + @Mock + private TopTaskTracker mTopTaskTracker; + + // Class under test + private RecentTasksList mRecentTasksList; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class); + KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); + + // Set desktop mode supported + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true); + when(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops)) + .thenReturn(true); + + mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor, + mockKeyguardManager, mSystemUiProxy, mTopTaskTracker, + mock(DesktopVisibilityController.class), + mock(DaggerSingletonTracker.class)); + } + + @Test + public void onRecentTasksChanged_doesNotFetchTasks() throws Exception { + mRecentTasksList.onRecentTasksChanged(); + verify(mSystemUiProxy, times(0)) + .getRecentTasks(anyInt(), anyInt()); + } + + @Test + public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception { + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks( + new RecentTaskInfo(), new RecentTaskInfo(), new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + true); + + assertEquals(1, taskList.size()); + taskList.get(0).getTasks().forEach(t -> assertNull(t.taskDescription.getLabel())); + } + + @Test + public void loadTasksInBackground_GetRecentTasksException() throws Exception { + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed")); + + RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE, -1, false); + + assertThat(taskList.mRequestId).isEqualTo(-1); + assertThat(taskList).isEmpty(); + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void loadTasksInBackground_freeformTask_multiDesksInMultiDisplays() throws Exception { + List tasksInDefaultDesk1 = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 1, DEFAULT_DISPLAY), + createRecentTaskInfo(/* taskId = */ 4, DEFAULT_DISPLAY)); + List tasksInDefaultDesk2 = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 2, DEFAULT_DISPLAY), + createRecentTaskInfo(/* taskId = */ 3, DEFAULT_DISPLAY)); + List tasksInExtend = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 5, /* displayId = */ 1), + createRecentTaskInfo(/* taskId = */ 6, /* displayId = */ 1)); + GroupedTaskInfo recentTaskInfosOfDesk1 = GroupedTaskInfo.forDeskTasks(/* deskId = */1, + DEFAULT_DISPLAY, tasksInDefaultDesk1, /* minimizedTaskIds = */ + Collections.emptySet()); + GroupedTaskInfo recentTaskInfosOfDesk2 = GroupedTaskInfo.forDeskTasks(/* deskId = */2, + DEFAULT_DISPLAY, tasksInDefaultDesk2, /* minimizedTaskIds = */ + Collections.emptySet()); + GroupedTaskInfo recentTaskInfosOfDesk3 = GroupedTaskInfo.forDeskTasks(/* deskId = */3, + /* displayId = */ 1, tasksInExtend, /* minimizedTaskIds = */ + Collections.emptySet()); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())).thenReturn( + new ArrayList<>(Arrays.asList(recentTaskInfosOfDesk1, recentTaskInfosOfDesk2, + recentTaskInfosOfDesk3))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + false); + + assertThat(taskList).hasSize(3); + assertThat(taskList.get(2).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk1 = taskList.get(2).getTasks(); + assertThat(actualFreeformTasksInDesk1).hasSize(2); + assertThat(actualFreeformTasksInDesk1.get(0).key.id).isEqualTo(1); + assertThat(actualFreeformTasksInDesk1.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk1.get(1).key.id).isEqualTo(4); + assertThat(actualFreeformTasksInDesk1.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(2)).getDeskId()).isEqualTo(1); + assertThat(((DesktopTask) taskList.get(2)).getDisplayId()).isEqualTo(DEFAULT_DISPLAY); + + assertThat(taskList.get(1).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk2 = taskList.get(1).getTasks(); + assertThat(actualFreeformTasksInDesk2).hasSize(2); + assertThat(actualFreeformTasksInDesk2.get(0).key.id).isEqualTo(2); + assertThat(actualFreeformTasksInDesk2.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk2.get(1).key.id).isEqualTo(3); + assertThat(actualFreeformTasksInDesk2.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(1)).getDeskId()).isEqualTo(2); + assertThat(((DesktopTask) taskList.get(1)).getDisplayId()).isEqualTo(DEFAULT_DISPLAY); + + assertThat(taskList.get(0).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk3 = taskList.get(0).getTasks(); + assertThat(actualFreeformTasksInDesk3).hasSize(2); + assertThat(actualFreeformTasksInDesk3.get(0).key.id).isEqualTo(5); + assertThat(actualFreeformTasksInDesk3.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk3.get(1).key.id).isEqualTo(6); + assertThat(actualFreeformTasksInDesk3.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(0)).getDeskId()).isEqualTo(3); + assertThat(((DesktopTask) taskList.get(0)).getDisplayId()).isEqualTo(1); + } + + @Test + public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception { + String taskDescription = "Wheeee!"; + RecentTaskInfo task1 = new RecentTaskInfo(); + task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); + RecentTaskInfo task2 = new RecentTaskInfo(); + task2.taskDescription = new ActivityManager.TaskDescription(); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2, + new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + false); + + assertEquals(1, taskList.size()); + var tasks = taskList.get(0).getTasks(); + assertEquals(2, tasks.size()); + assertEquals(taskDescription, tasks.get(0).taskDescription.getLabel()); + assertNull(tasks.get(1).taskDescription.getLabel()); + } + + @Test + @DisableFlags({FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS, + FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND}) + public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception { + List tasks = Arrays.asList( + createRecentTaskInfo(1 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(5 /* taskId */, 1 /* displayId */), + createRecentTaskInfo(6 /* taskId */, 1 /* displayId */)); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks( + 0 /* deskId */, DEFAULT_DISPLAY, tasks, + Collections.emptySet() /* minimizedTaskIds */); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(1, taskList.size()); + assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasks = taskList.get(0).getTasks(); + assertEquals(4, actualFreeformTasks.size()); + assertEquals(1, actualFreeformTasks.get(0).key.id); + assertFalse(actualFreeformTasks.get(0).isMinimized); + assertEquals(4, actualFreeformTasks.get(1).key.id); + assertFalse(actualFreeformTasks.get(1).isMinimized); + assertEquals(5, actualFreeformTasks.get(2).key.id); + assertFalse(actualFreeformTasks.get(2).isMinimized); + assertEquals(6, actualFreeformTasks.get(3).key.id); + assertFalse(actualFreeformTasks.get(3).isMinimized); + } + + @Test + @EnableFlags(FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS) + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void loadTasksInBackground_freeformTask_createsDesktopTaskPerDisplay() throws Exception { + List tasks = Arrays.asList( + createRecentTaskInfo(1 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(5 /* taskId */, 1 /* displayId */), + createRecentTaskInfo(6 /* taskId */, 1 /* displayId */)); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks( + 0 /* deskId */, DEFAULT_DISPLAY, tasks, + Collections.emptySet() /* minimizedTaskIds */); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(2, taskList.size()); + assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasksDefaultDisplay = taskList.get(0).getTasks(); + assertEquals(2, actualFreeformTasksDefaultDisplay.size()); + assertEquals(1, actualFreeformTasksDefaultDisplay.get(0).key.id); + assertFalse(actualFreeformTasksDefaultDisplay.get(0).isMinimized); + assertEquals(4, actualFreeformTasksDefaultDisplay.get(1).key.id); + assertFalse(actualFreeformTasksDefaultDisplay.get(1).isMinimized); + + List actualFreeformTasksExternalDisplay = taskList.get(1).getTasks(); + assertEquals(2, actualFreeformTasksExternalDisplay.size()); + assertEquals(5, actualFreeformTasksExternalDisplay.get(0).key.id); + assertFalse(actualFreeformTasksExternalDisplay.get(0).isMinimized); + assertEquals(6, actualFreeformTasksExternalDisplay.get(1).key.id); + assertFalse(actualFreeformTasksExternalDisplay.get(1).isMinimized); + } + + @Test + public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_createDesktopTask() + throws Exception { + List tasks = Arrays.asList( + createRecentTaskInfo(1 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(5 /* taskId */, DEFAULT_DISPLAY)); + Set minimizedTaskIds = + Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet()); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks( + 0 /* deskId */, DEFAULT_DISPLAY, tasks, minimizedTaskIds); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(1, taskList.size()); + assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasks = taskList.get(0).getTasks(); + assertEquals(3, actualFreeformTasks.size()); + assertEquals(1, actualFreeformTasks.get(0).key.id); + assertTrue(actualFreeformTasks.get(0).isMinimized); + assertEquals(4, actualFreeformTasks.get(1).key.id); + assertTrue(actualFreeformTasks.get(1).isMinimized); + assertEquals(5, actualFreeformTasks.get(2).key.id); + assertTrue(actualFreeformTasks.get(2).isMinimized); + } + + private TaskInfo createRecentTaskInfo(int taskId, int displayId) { + RecentTaskInfo recentTaskInfo = new RecentTaskInfo(); + recentTaskInfo.taskId = taskId; + recentTaskInfo.displayId = displayId; + return recentTaskInfo; + } +} diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt similarity index 55% rename from quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt index 80fbce726..a7370b02e 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt @@ -1,15 +1,20 @@ package com.android.quickstep -import android.content.Context -import android.testing.AndroidTestingRunner -import androidx.test.core.app.ApplicationProvider +import android.view.Display +import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest +import com.android.launcher3.dagger.LauncherComponentProvider import com.android.launcher3.util.DisplayController.CHANGE_DENSITY import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE import com.android.launcher3.util.DisplayController.CHANGE_ROTATION import com.android.launcher3.util.DisplayController.Info +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.NavigationMode +import com.android.launcher3.util.SandboxApplication import com.android.quickstep.util.GestureExclusionManager +import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION @@ -22,7 +27,9 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SE import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -30,24 +37,43 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever /** Unit test for [RecentsAnimationDeviceState]. */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) class RecentsAnimationDeviceStateTest { + @get:Rule val context = SandboxApplication() + @Mock private lateinit var exclusionManager: GestureExclusionManager @Mock private lateinit var info: Info - private val context = ApplicationProvider.getApplicationContext() as Context private lateinit var underTest: RecentsAnimationDeviceState @Before fun setup() { MockitoAnnotations.initMocks(this) - underTest = RecentsAnimationDeviceState(context, exclusionManager) + + val component = LauncherComponentProvider.get(context) + underTest = + RecentsAnimationDeviceState( + context, + exclusionManager, + component.displayController, + component.contextualSearchStateManager, + component.rotationTouchHelper, + component.settingsCache, + component.daggerSingletonTracker, + ) + } + + @After + fun tearDown() { + UI_HELPER_EXECUTOR.submit {}.get() + MAIN_EXECUTOR.submit {}.get() } @Test @@ -64,7 +90,7 @@ class RecentsAnimationDeviceStateTest { underTest.registerExclusionListener() - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test @@ -85,7 +111,7 @@ class RecentsAnimationDeviceStateTest { underTest.unregisterExclusionListener() - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test @@ -116,33 +142,33 @@ class RecentsAnimationDeviceStateTest { underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY) - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test fun trackpadGesturesNotAllowedForSelectedStates() { - val disablingStates = GESTURE_DISABLING_SYSUI_STATES + - SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED + val disablingStates = + GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED allSysUiStates().forEach { state -> val canStartGesture = !disablingStates.contains(state) - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlagsForDisplay(state, Display.DEFAULT_DISPLAY) assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture) } } @Test fun trackpadGesturesNotAllowedIfHomeAndOverviewIsDisabled() { - val stateToExpectedResult = mapOf( - SYSUI_STATE_HOME_DISABLED to true, - SYSUI_STATE_OVERVIEW_DISABLED to true, - DEFAULT_STATE - .enable(SYSUI_STATE_OVERVIEW_DISABLED) - .enable(SYSUI_STATE_HOME_DISABLED) to false - ) + val stateToExpectedResult = + mapOf( + SYSUI_STATE_HOME_DISABLED to true, + SYSUI_STATE_OVERVIEW_DISABLED to true, + DEFAULT_STATE.enable(SYSUI_STATE_OVERVIEW_DISABLED) + .enable(SYSUI_STATE_HOME_DISABLED) to false, + ) stateToExpectedResult.forEach { (state, allowed) -> - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlagsForDisplay(state, Display.DEFAULT_DISPLAY) assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed) } } @@ -153,48 +179,77 @@ class RecentsAnimationDeviceStateTest { allSysUiStates().forEach { state -> val canStartGesture = !disablingStates.contains(state) - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlagsForDisplay(state, Display.DEFAULT_DISPLAY) assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture) } } @Test fun systemGesturesNotAllowedWhenGestureStateDisabledAndNavBarVisible() { - val stateToExpectedResult = mapOf( - DEFAULT_STATE - .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false, - ) + val stateToExpectedResult = + mapOf( + DEFAULT_STATE.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false, + ) - stateToExpectedResult.forEach {(state, gestureAllowed) -> - underTest.setSystemUiFlags(state) + stateToExpectedResult.forEach { (state, gestureAllowed) -> + underTest.setSysUIStateFlagsForDisplay(state, Display.DEFAULT_DISPLAY) assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed) } } + @Test + fun getSystemUiStateFlags_defaultAwake() { + val NOT_EXISTENT_DISPLAY = 2 + assertThat(underTest.getSystemUiStateFlags(NOT_EXISTENT_DISPLAY)) + .isEqualTo(QuickStepContract.SYSUI_STATE_AWAKE) + } + + @Test + fun clearSysUIStateFlagsForDisplay_displayNotReturnedAnymore() { + underTest.setSysUIStateFlagsForDisplay(1, /* displayId= */ 1) + + assertThat(underTest.displaysWithSysUIState).contains(1) + assertThat(underTest.getSystemUiStateFlags(1)).isEqualTo(1) + + underTest.clearSysUIStateFlagsForDisplay(1) + + assertThat(underTest.displaysWithSysUIState).doesNotContain(1) + assertThat(underTest.getSystemUiStateFlags(1)) + .isEqualTo(QuickStepContract.SYSUI_STATE_AWAKE) + } + + @Test + fun setSysUIStateFlagsForDisplay_setsCorrectly() { + underTest.setSysUIStateFlagsForDisplay(1, /* displayId= */ 1) + underTest.setSysUIStateFlagsForDisplay(2, /* displayId= */ 2) + + assertThat(underTest.getSystemUiStateFlags(1)).isEqualTo(1) + assertThat(underTest.getSystemUiStateFlags(2)).isEqualTo(2) + assertThat(underTest.displaysWithSysUIState).containsAtLeast(1, 2) + } + private fun allSysUiStates(): List { // SYSUI_STATES_* are binary flags return (0..SYSUI_STATES_COUNT).map { 1L shl it } } companion object { - private val GESTURE_DISABLING_SYSUI_STATES = listOf( - SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, - SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, - SYSUI_STATE_QUICK_SETTINGS_EXPANDED, - SYSUI_STATE_MAGNIFICATION_OVERLAP, - SYSUI_STATE_DEVICE_DREAMING, - SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION, - ) + private val GESTURE_DISABLING_SYSUI_STATES = + listOf( + SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, + SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, + SYSUI_STATE_QUICK_SETTINGS_EXPANDED, + SYSUI_STATE_MAGNIFICATION_OVERLAP, + SYSUI_STATE_DEVICE_DREAMING, + SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION, + ) private const val SYSUI_STATES_COUNT = 33 private const val DEFAULT_STATE = 0L } diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java similarity index 75% rename from quickstep/tests/src/com/android/quickstep/RecentsModelTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java index 648fa932a..2eb2e4c2c 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java @@ -32,22 +32,30 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; +import android.graphics.Rect; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.graphics.ThemeManager; import com.android.launcher3.icons.IconProvider; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.LockedUserState; +import com.android.launcher3.util.SplitConfigurationOptions; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SplitTask; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.wm.shell.shared.split.SplitScreenConstants; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -56,6 +64,7 @@ import java.util.ArrayList; import java.util.function.Consumer; @SmallTest +@RunWith(AndroidJUnit4.class) public class RecentsModelTest { @Mock private Context mContext; @@ -67,7 +76,13 @@ public class RecentsModelTest { private RecentTasksList mTasksList; @Mock - private TaskThumbnailCache.HighResLoadingState mHighResLoadingState; + private HighResLoadingState mHighResLoadingState; + + @Mock + private LockedUserState mLockedUserState; + + @Mock + private ThemeManager mThemeManager; private RecentsModel mRecentsModel; @@ -94,7 +109,8 @@ public class RecentsModelTest { when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true); mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class), - mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class)); + mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class), + mLockedUserState, () -> mThemeManager, mock(DaggerSingletonTracker.class)); mResource = mock(Resources.class); when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3); @@ -111,10 +127,12 @@ public class RecentsModelTest { .updateThumbnailInCache(taskArgs.capture(), /* lowResolution= */ eq(false)); GroupTask expectedGroupTask = mTaskResult.get(0); - assertThat(taskArgs.getAllValues().get(0)).isEqualTo( - expectedGroupTask.task1); - assertThat(taskArgs.getAllValues().get(1)).isEqualTo( - expectedGroupTask.task2); + var taskArgsValues = taskArgs.getAllValues(); + var expectedTasks = expectedGroupTask.getTasks(); + assertThat(taskArgsValues.size()).isEqualTo(expectedTasks.size()); + for (int i = 0; i < expectedTasks.size(); ++i) { + assertThat(taskArgsValues.get(i)).isEqualTo(expectedTasks.get(i)); + } } @Test @@ -157,6 +175,17 @@ public class RecentsModelTest { .updateThumbnailInCache(any(), anyBoolean()); } + @Test + public void themeCallbackAttachedOnUnlock() { + verify(mThemeManager, never()).addChangeListener(any()); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mLockedUserState).runOnUserUnlocked(callbackCaptor.capture()); + + callbackCaptor.getAllValues().forEach(Runnable::run); + verify(mThemeManager, times(1)).addChangeListener(any()); + } + private RecentTasksList.TaskLoadResult getTaskResult() { RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1); ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo(); @@ -167,7 +196,13 @@ public class RecentsModelTest { Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2); Task task2 = Task.from(taskKey2, taskInfo2, false); - allTasks.add(new GroupTask(task1, task2, null)); + allTasks.add( + new SplitTask(task1, task2, new SplitConfigurationOptions.SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50))); return allTasks; } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java new file mode 100644 index 000000000..c1be1cee9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java @@ -0,0 +1,94 @@ +/* + * 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.quickstep; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.util.AllModulesForTest; +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsState; +import com.android.quickstep.fallback.window.RecentsDisplayModel; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler; +import com.android.quickstep.views.RecentsViewContainer; + +import dagger.BindsInstance; +import dagger.Component; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class RecentsWindowSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase< + RecentsState, + RecentsWindowManager, + FallbackRecentsView, + RecentsWindowSwipeHandler, + FallbackWindowInterface> { + + @Mock private RecentsDisplayModel mRecentsDisplayModel; + @Mock private FallbackRecentsView mRecentsView; + @Mock private RecentsWindowManager mRecentsWindowManager; + + @Before + public void setRecentsDisplayModel() { + mContext.initDaggerComponent(DaggerRecentsWindowSwipeHandlerTestCase_TestComponent.builder() + .bindRecentsDisplayModel(mRecentsDisplayModel)); + } + + @NonNull + @Override + protected RecentsWindowSwipeHandler createSwipeHandler(long touchTimeMs, + boolean continuingLastGesture) { + return new RecentsWindowSwipeHandler( + mContext, + mTaskAnimationManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected RecentsViewContainer getRecentsContainer() { + return mRecentsWindowManager; + } + + @NonNull + @Override + protected FallbackRecentsView getRecentsView() { + return mRecentsView; + } + + @LauncherAppSingleton + @Component(modules = {AllModulesForTest.class}) + interface TestComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindRecentsDisplayModel(RecentsDisplayModel model); + @Override LauncherAppComponent build(); + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java new file mode 100644 index 000000000..75b59d734 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java @@ -0,0 +1,147 @@ +/* + * 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.quickstep; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.Display; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskAnimationManagerTest { + + protected final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + @Mock + private SystemUiProxy mSystemUiProxy; + + private TaskAnimationManager mTaskAnimationManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskAnimationManager = new TaskAnimationManager(mContext, + RecentsAnimationDeviceState.INSTANCE.get(mContext), Display.DEFAULT_DISPLAY) { + @Override + SystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + }; + } + + @Test + public void startRecentsActivity_allowBackgroundLaunch() { + final LauncherActivityInterface activityInterface = mock(LauncherActivityInterface.class); + final GestureState gestureState = mock(GestureState.class); + final RecentsAnimationCallbacks.RecentsAnimationListener listener = + mock(RecentsAnimationCallbacks.RecentsAnimationListener.class); + doReturn(activityInterface).when(gestureState).getContainerInterface(); + runOnMainSync(() -> + mTaskAnimationManager.startRecentsAnimation(gestureState, new Intent(), listener)); + final ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(ActivityOptions.class); + verify(mSystemUiProxy) + .startRecentsActivity(any(), optionsCaptor.capture(), any(), anyBoolean()); + assertEquals(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS, + optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode()); + } + + @Test + public void testLauncherDestroyed_whileRecentsAnimationStartPending_finishesAnimation() { + final GestureState gestureState = mock(GestureState.class); + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(RecentsAnimationCallbacks.class); + final RecentsAnimationControllerCompat controllerCompat = + mock(RecentsAnimationControllerCompat.class); + final RemoteAnimationTarget remoteAnimationTarget = new RemoteAnimationTarget( + /* taskId= */ 0, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ new ActivityManager.RunningTaskInfo(), + /* allowEnterPip= */ false); + + doReturn(mock(LauncherActivityInterface.class)).when(gestureState).getContainerInterface(); + when(mSystemUiProxy + .startRecentsActivity(any(), any(), listenerCaptor.capture(), anyBoolean())) + .thenReturn(true); + when(gestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]); + + runOnMainSync(() -> { + mTaskAnimationManager.startRecentsAnimation( + gestureState, + new Intent(), + mock(RecentsAnimationCallbacks.RecentsAnimationListener.class)); + mTaskAnimationManager.onLauncherDestroyed(); + listenerCaptor.getValue().onAnimationStart( + controllerCompat, + new RemoteAnimationTarget[] { remoteAnimationTarget }, + new RemoteAnimationTarget[] { remoteAnimationTarget }, + new Rect(), + new Rect(), + new Bundle(), + new TransitionInfo(0, 0)); + }); + runOnMainSync(() -> verify(controllerCompat) + .finish(/* toHome= */ eq(false), anyBoolean(), any())); + } + + protected static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java similarity index 96% rename from quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java index 4e04261e5..3686c16ad 100644 --- a/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.R; @@ -35,12 +36,14 @@ import com.android.quickstep.util.TaskKeyCache; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.concurrent.Executor; @SmallTest +@RunWith(AndroidJUnit4.class) public class TaskThumbnailCacheTest { @Mock private Context mContext; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt new file mode 100644 index 000000000..4e9dae8da --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.fallback + +import com.android.launcher3.testing.shared.TestProtocol.BACKGROUND_APP_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class RecentsStateUtilsTest { + + @Test + fun testRecentsStateDefault_toLauncherStateOrdinal_isOverviewStateOrdinal() { + assertThat(RecentsState.DEFAULT.toLauncherStateOrdinal()).isEqualTo(OVERVIEW_STATE_ORDINAL) + } + + @Test + fun testRecentsStateModal_toLauncherStateOrdinal_isModalTaskStateOrdinal() { + assertThat(RecentsState.MODAL_TASK.toLauncherStateOrdinal()) + .isEqualTo(OVERVIEW_MODAL_TASK_STATE_ORDINAL) + } + + @Test + fun testRecentsStateBackgroundApp_toLauncherStateOrdinal_isBackgroundAppStateOrdinal() { + assertThat(RecentsState.BACKGROUND_APP.toLauncherStateOrdinal()) + .isEqualTo(BACKGROUND_APP_STATE_ORDINAL) + } + + @Test + fun testRecentsStateHome_toLauncherStateOrdinal_isNormalStateOrdinal() { + assertThat(RecentsState.HOME.toLauncherStateOrdinal()).isEqualTo(NORMAL_STATE_ORDINAL) + } + + @Test + fun testRecentsStateBgLauncher_toLauncherStateOrdinal_isNormalStateOrdinal() { + assertThat(RecentsState.BG_LAUNCHER.toLauncherStateOrdinal()) + .isEqualTo(NORMAL_STATE_ORDINAL) + } + + @Test + fun testRecentsStateOverviewSplitSelect_toLauncherStateOrdinal_isOverviewSplitSelectStateOrdinal() { + assertThat(RecentsState.OVERVIEW_SPLIT_SELECT.toLauncherStateOrdinal()) + .isEqualTo(OVERVIEW_SPLIT_SELECT_ORDINAL) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java new file mode 100644 index 000000000..901877531 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.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.quickstep.inputconsumers; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.NavHandle; +import com.android.quickstep.util.TestExtensions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressHandlerTest { + + private NavHandleLongPressHandler mLongPressHandler; + @Mock private NavHandle mNavHandle; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mLongPressHandler = new NavHandleLongPressHandler(context); + } + + @Test + public void testStartNavBarAnimation_flagDisabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(false)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle, never()) + .animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testStartNavBarAnimation_flagEnabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(true)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle).animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideAnimateLPNHFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ANIMATE_LPNH", value, () -> DeviceConfigWrapper.get().getAnimateLpnh()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java new file mode 100644 index 000000000..ee9505cdb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java @@ -0,0 +1,545 @@ +/* + * 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.quickstep.inputconsumers; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_HOVER_ENTER; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS; +import static com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer.MIN_TIME_TO_LOG_ABANDON_MS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.os.SystemClock; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.AllModulesForTest; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.SandboxContext; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.GestureState; +import com.android.quickstep.InputConsumer; +import com.android.quickstep.NavHandle; +import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.TestExtensions; +import com.android.systemui.shared.system.InputMonitorCompat; + +import dagger.BindsInstance; +import dagger.Component; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.atomic.AtomicBoolean; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressInputConsumerTest { + + private static final float TOUCH_SLOP = 10; + private static final float SQUARED_TOUCH_SLOP = 100; + + private final AtomicBoolean mLongPressTriggered = new AtomicBoolean(); + private final Runnable mLongPressRunnable = () -> mLongPressTriggered.set(true); + private NavHandleLongPressInputConsumer mUnderTest; + private SandboxContext mContext; + private float mScreenWidth; + private long mDownTimeMs; + @Mock InputConsumer mDelegate; + @Mock InputMonitorCompat mInputMonitor; + @Mock RecentsAnimationDeviceState mDeviceState; + @Mock NavHandle mNavHandle; + @Mock GestureState mGestureState; + @Mock NavHandleLongPressHandler mNavHandleLongPressHandler; + @Mock TopTaskTracker mTopTaskTracker; + @Mock TopTaskTracker.CachedTaskInfo mTaskInfo; + @Mock StatsLogManager mStatsLogManager; + @Mock StatsLogManager.StatsLogger mStatsLogger; + @Mock StatsLogManager.StatsLatencyLogger mStatsLatencyLogger; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mTopTaskTracker.getCachedTopTask(anyBoolean(), anyInt())).thenReturn(mTaskInfo); + when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP); + when(mDelegate.allowInterceptByParent()).thenReturn(true); + mLongPressTriggered.set(false); + when(mNavHandleLongPressHandler.getLongPressRunnable(any(), anyInt())).thenReturn( + mLongPressRunnable); + when(mStatsLogger.withPackageName(any())).thenReturn(mStatsLogger); + when(mStatsLatencyLogger.withInstanceId(any())).thenReturn(mStatsLatencyLogger); + when(mStatsLatencyLogger.withLatency(anyLong())).thenReturn(mStatsLatencyLogger); + when(mStatsLogManager.logger()).thenReturn(mStatsLogger); + when(mStatsLogManager.latencyLogger()).thenReturn(mStatsLatencyLogger); + initializeObjectUnderTest(); + } + + @After + public void tearDown() throws Exception { + MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable); + MAIN_EXECUTOR.submit(() -> null).get(); + mContext.onDestroy(); + } + + @Test + public void testGetType() { + assertThat(mUnderTest.getType() & InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS).isNotEqualTo(0); + } + + @Test + public void testDelegateDisallowsTouchIntercept() { + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + + verify(mDelegate).onMotionEvent(any()); + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testDelegateDisallowsTouchInterceptAfterTouchDown() { + // Touch down and wait the minimum abandonment time. + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + // Delegate should still get touches unless long press is triggered. + verify(mDelegate).onMotionEvent(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + + // Child delegate blocks us from intercepting further motion events. + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE)); + + // Delegate should still get motion events unless long press is triggered. + verify(mDelegate, times(2)).onMotionEvent(any()); + // But our handler should be cancelled. + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + // Because we handled touch down before the child blocked additional events, log abandon. + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressTriggered() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + + // Ensure abandon latency is still not logged after long press. + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithSlightVerticalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, 1)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithSlightHorizontalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, mScreenWidth / 2f + 1, 0)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithExtendedTwoStageDuration() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP - 1), 0)); + // We have entered the second stage, so the normal timeout shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + + // After an extended time, the long press should trigger. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressTriggeredWithNormalDurationInFirstStage() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // We have not entered the second stage, so the normal timeout should trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressAbortedByTouchUp() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchCancel() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testTouchCancelWithoutTouchDown() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL)); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedVertically() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP + 1))); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedHorizontally() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP + 1), 0)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedVertically_twoStageEnabled() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // Enter the second stage. + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP - 1))); + // Normal duration shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + // Move out of the second stage. + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP + 1))); + // Wait past the extended long press timeout, to be sure it wouldn't have triggered. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + // Touch cancelled. + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressAbortedByTouchSlopPassedHorizontally_twoStageEnabled() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // Enter the second stage. + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP - 1), 0)); + // Normal duration shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + // Move out of the second stage. + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP + 1), 0)); + // Wait past the extended long press timeout, to be sure it wouldn't have triggered. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + // Touch cancelled. + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testTouchOutsideNavHandleIgnored() { + // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily) + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + // Should be ignored because the x position was not centered in the navbar region. + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testHoverPassedToDelegate() { + // Regardless of whether the delegate wants us to intercept, we tell it about hover events. + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER)); + + verify(mDelegate).onHoverEvent(any()); + + when(mDelegate.allowInterceptByParent()).thenReturn(true); + mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER)); + + verify(mDelegate, times(2)).onHoverEvent(any()); + + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testNoLogsForShortTouch() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(10); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + private void initializeObjectUnderTest() { + if (mContext != null) { + mContext.onDestroy(); + } + mContext = new SandboxContext(getApplicationContext()); + mContext.initDaggerComponent( + DaggerNavHandleLongPressInputConsumerTest_TopTaskTrackerComponent + .builder() + .bindTopTaskTracker(mTopTaskTracker)); + mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x; + mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor, + mDeviceState, mNavHandle, mGestureState); + mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler); + mUnderTest.setStatsLogManager(mStatsLogManager); + mDownTimeMs = 0; + } + + private static void sleep(long sleepMs) { + SystemClock.sleep(sleepMs); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + /** Generate a motion event centered horizontally in the screen. */ + private MotionEvent generateCenteredMotionEvent(int motionAction) { + return generateCenteredMotionEventWithYOffset(motionAction, 0); + } + + /** Generate a motion event centered horizontally in the screen, with y offset. */ + private MotionEvent generateCenteredMotionEventWithYOffset(int motionAction, float y) { + return generateMotionEvent(motionAction, mScreenWidth / 2f, y); + } + + private MotionEvent generateMotionEvent(int motionAction, float x, float y) { + if (motionAction == ACTION_DOWN) { + mDownTimeMs = SystemClock.uptimeMillis(); + } + long eventTime = SystemClock.uptimeMillis(); + return MotionEvent.obtain(mDownTimeMs, eventTime, motionAction, x, y, 0); + } + + private static AutoCloseable overrideTwoStageFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_LPNH_TWO_STAGES", + value, + () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages()); + } + + @LauncherAppSingleton + @Component(modules = AllModulesForTest.class) + public interface TopTaskTrackerComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindTopTaskTracker(TopTaskTracker topTaskTracker); + + @Override + TopTaskTrackerComponent build(); + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt new file mode 100644 index 000000000..14570b54e --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt @@ -0,0 +1,175 @@ +/* + * 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.quickstep.logging + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.LauncherPrefs.Companion.ALLOW_ROTATION +import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY +import com.android.launcher3.graphics.ThemeManager +import com.android.launcher3.logging.InstanceId +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_DISABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED +import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.SettingsCache +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class SettingsChangeLoggerTest { + private val mContext: Context = ApplicationProvider.getApplicationContext() + + private val mInstanceId = InstanceId.fakeInstanceId(1) + + private lateinit var mSystemUnderTest: SettingsChangeLogger + + @Mock private lateinit var mStatsLogManager: StatsLogManager + + @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger + @Mock private lateinit var mTracker: DaggerSingletonTracker + private var displayController: DisplayController = DisplayController.INSTANCE.get(mContext) + private var settingsCache: SettingsCache = SettingsCache.INSTANCE.get(mContext) + + @Captor private lateinit var mEventCaptor: ArgumentCaptor + + private var mDefaultThemedIcons = false + private var mDefaultAllowRotation = false + + private val themeManager: ThemeManager + get() = ThemeManager.INSTANCE.get(mContext) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(mStatsLogManager.logger()).doReturn(mMockLogger) + whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger) + mDefaultThemedIcons = themeManager.isMonoThemeEnabled + mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION) + // To match the default value of THEMED_ICONS + themeManager.isMonoThemeEnabled = false + // To match the default value of ALLOW_ROTATION + LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false) + + mSystemUnderTest = + SettingsChangeLogger( + mContext, + mStatsLogManager, + mTracker, + displayController, + settingsCache, + ) + } + + @After + fun tearDown() { + themeManager.isMonoThemeEnabled = mDefaultThemedIcons + LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation) + } + + @Test + fun loggingPrefs_correctDefaultValue() { + val systemUnderTest = + SettingsChangeLogger( + mContext, + mStatsLogManager, + mTracker, + displayController, + settingsCache, + ) + + assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue) + .isFalse() + assertThat(systemUnderTest.loggingPrefs[ADD_ICON_PREFERENCE_KEY]!!.defaultValue).isTrue() + assertThat(systemUnderTest.loggingPrefs[OVERVIEW_SUGGESTED_ACTIONS]!!.defaultValue).isTrue() + assertThat(systemUnderTest.loggingPrefs[SMARTSPACE_ON_HOME_SCREEN]!!.defaultValue).isTrue() + assertThat(systemUnderTest.loggingPrefs[KEY_ENABLE_MINUS_ONE]!!.defaultValue).isTrue() + } + + @Test + fun logSnapshot_defaultValue() { + mSystemUnderTest.logSnapshot(mInstanceId) + + verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture()) + val capturedEvents = mEventCaptor.allValues + assertThat(capturedEvents.isNotEmpty()).isTrue() + verifyDefaultEvent(capturedEvents) + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id }) + .isTrue() + } + + @Test + fun logSnapshot_updateAllowRotation() { + LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true) + + // This a new object so the values of mLoggablePrefs will be different + SettingsChangeLogger(mContext, mStatsLogManager, mTracker, displayController, settingsCache) + .logSnapshot(mInstanceId) + + verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture()) + val capturedEvents = mEventCaptor.allValues + assertThat(capturedEvents.isNotEmpty()).isTrue() + verifyDefaultEvent(capturedEvents) + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_ENABLED.id }) + .isTrue() + } + + private fun verifyDefaultEvent(capturedEvents: MutableList) { + assertThat(capturedEvents.any { it.id == LAUNCHER_NOTIFICATION_DOT_ENABLED.id }).isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_THEMED_ICON_DISABLED.id }).isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED }).isTrue() + } + + companion object { + private const val KEY_ENABLE_MINUS_ONE = "pref_enable_minus_one" + private const val OVERVIEW_SUGGESTED_ACTIONS = "pref_overview_action_suggestions" + private const val SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen" + + private const val LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED = 617 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt index ea52842e2..66b3b047d 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt @@ -43,12 +43,12 @@ class LandscapePagedViewHandlerTest { if (isEnabled) { setFlagsRule.enableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } else { setFlagsRule.disableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } } @@ -62,6 +62,7 @@ class LandscapePagedViewHandlerTest { isRTL, OVERVIEW_TASK_MARGIN_PX, DIVIDER_SIZE_PX, + oneIconHiddenDueToSmallWidth = false, ) } @@ -107,14 +108,8 @@ class LandscapePagedViewHandlerTest { val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) - // TODO(b/326377497): When started in fake seascape and rotated to landscape, - // the icon chips are in RTL and wrongly positioned at the right side of the snapshot. - // Top-Left app chip should be placed at the top left of the first snapshot, but because - // this issue, it's displayed at the top-right of the second snapshot. - // The Bottom-Right app chip is displayed at the top-right of the first snapshot because - // of this issue. - assertThat(topLeftY).isEqualTo(0) - assertThat(bottomRightY).isEqualTo(-316) + assertThat(topLeftY).isEqualTo(-316) + assertThat(bottomRightY).isEqualTo(0) } /** Test updateSplitIconsPosition */ diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt index 2bc182c02..d455b0d1e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt @@ -43,12 +43,12 @@ class SeascapePagedViewHandlerTest { if (isEnabled) { setFlagsRule.enableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } else { setFlagsRule.disableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } } @@ -62,6 +62,7 @@ class SeascapePagedViewHandlerTest { isRTL, OVERVIEW_TASK_MARGIN_PX, DIVIDER_SIZE_PX, + oneIconHiddenDueToSmallWidth = false, ) } @@ -109,12 +110,6 @@ class SeascapePagedViewHandlerTest { val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) - // TODO(b/326377497): When started in fake seascape and rotated to landscape, - // the icon chips are in RTL and wrongly positioned at the right side of the snapshot. - // Top-Left app chip should be placed at the top left of the first snapshot, but because - // this issue, it's displayed at the top-right of the second snapshot. - // The Bottom-Right app chip is displayed at the top-right of the first snapshot because - // of this issue. assertThat(topLeftY).isEqualTo(316) assertThat(bottomRightY).isEqualTo(0) } @@ -166,7 +161,7 @@ class SeascapePagedViewHandlerTest { `when`(iconView.layoutParams).thenReturn(frameLayout) sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) - assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START) + assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.END) verify(iconView).setSplitTranslationX(0f) verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) } @@ -181,7 +176,7 @@ class SeascapePagedViewHandlerTest { `when`(iconView.layoutParams).thenReturn(frameLayout) sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) - assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END) + assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.START) verify(iconView).setSplitTranslationX(0f) verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) } diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt similarity index 51% rename from tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt index d3d297003..4adf01ef3 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 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,32 +14,18 @@ * limitations under the License. */ -package com.android.launcher3.celllayout.board; +package com.android.quickstep.recents.data -import android.graphics.Point; +import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback -public class IconPoint { - public Point coord; - public char mType; +class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier { + val listeners = mutableListOf() - public IconPoint(Point coord, char type) { - this.coord = coord; - mType = type; + override fun addCallback(callback: HighResLoadingStateChangedCallback) { + listeners.add(callback) } - public char getType() { - return mType; - } - - public void setType(char type) { - mType = type; - } - - public Point getCoord() { - return coord; - } - - public void setCoord(Point coord) { - this.coord = coord; + override fun removeCallback(callback: HighResLoadingStateChangedCallback) { + listeners.remove(callback) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt index eaeb513ea..9e99a0bd6 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt @@ -20,10 +20,12 @@ import com.android.quickstep.util.GroupTask import java.util.function.Consumer class FakeRecentTasksDataSource : RecentTasksDataSource { - var taskList: List = listOf() + private var taskList: List = listOf() override fun getTasks(callback: Consumer>?): Int { - callback?.accept(taskList) + // Makes a copy of the GroupTask to create a new GroupTask instance and to simulate + // RecentsModel::getTasks behavior. + callback?.accept(taskList.map { it.copy() }) return 0 } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt new file mode 100644 index 000000000..4e909035b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.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.quickstep.recents.data + +class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository { + private var recentsDeviceProfile = + RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false) + + override fun getRecentsDeviceProfile() = recentsDeviceProfile + + fun setRecentsDeviceProfile(newValue: RecentsDeviceProfile) { + recentsDeviceProfile = newValue + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt new file mode 100644 index 000000000..c328672d9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt @@ -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. + */ + +package com.android.quickstep.recents.data + +import android.view.Surface + +class FakeRecentsRotationStateRepository : RecentsRotationStateRepository { + private var recentsRotationState = + RecentsRotationState( + activityRotation = Surface.ROTATION_0, + orientationHandlerRotation = Surface.ROTATION_0 + ) + + override fun getRecentsRotationState() = recentsRotationState + + fun setRecentsRotationState(newValue: RecentsRotationState) { + recentsRotationState = newValue + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt new file mode 100644 index 000000000..f6f158f54 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt @@ -0,0 +1,76 @@ +/* + * 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.quickstep.recents.data + +import android.graphics.drawable.Drawable +import com.android.quickstep.TaskIconCache.TaskCacheEntry +import com.android.quickstep.task.thumbnail.data.TaskIconDataSource +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.yield +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FakeTaskIconDataSource : TaskIconDataSource { + + val taskIdToDrawable: MutableMap = + (0..10).associateWith { mockCopyableDrawable() }.toMutableMap() + private val completionPrevented: MutableSet = mutableSetOf() + + /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */ + override suspend fun getIcon(task: Task): TaskCacheEntry { + while (task.key.id in completionPrevented) { + yield() + } + return TaskCacheEntry( + taskIdToDrawable.getValue(task.key.id), + "content desc ${task.key.id}", + "title ${task.key.id}", + ) + } + + fun preventIconLoad(taskId: Int) { + completionPrevented.add(taskId) + } + + fun completeLoadingForTask(taskId: Int) { + completionPrevented.remove(taskId) + } + + fun completeLoading() { + completionPrevented.clear() + } + + companion object { + fun mockCopyableDrawable(): Drawable { + val mutableDrawable = mock() + val immutableDrawable = + mock().apply { whenever(mutate()).thenReturn(mutableDrawable) } + val constantState = + mock().apply { + whenever(newDrawable()).thenReturn(immutableDrawable) + } + return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) } + } + } +} + +fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) { + assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id]) + assertThat(titleDescription).isEqualTo("content desc ${key.id}") + assertThat(title).isEqualTo("title ${key.id}") +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt index b66b7351b..40d5e0264 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -17,36 +17,41 @@ package com.android.quickstep.recents.data import android.graphics.Bitmap -import com.android.launcher3.util.CancellableTask import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData -import java.util.function.Consumer +import kotlinx.coroutines.yield import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { - val taskIdToBitmap: Map = (0..10).associateWith { mock() } - val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() - var shouldLoadSynchronously: Boolean = true + val taskIdToBitmap: MutableMap = + (0..10).associateWith { mock() }.toMutableMap() + private val completionPrevented: MutableSet = mutableSetOf() + private val getThumbnailCalls = mutableMapOf() + + var highResEnabled = true /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ - override fun updateThumbnailInBackground( - task: Task, - callback: Consumer - ): CancellableTask? { - val thumbnailData = mock() - whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id]) - val wrappedCallback = { - task.thumbnail = thumbnailData - callback.accept(thumbnailData) + override suspend fun getThumbnail(task: Task): ThumbnailData { + getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1 + + while (task.key.id in completionPrevented) { + yield() } - if (shouldLoadSynchronously) { - wrappedCallback() - } else { - taskIdToUpdatingTask[task.key.id] = wrappedCallback - } - return null + return ThumbnailData( + thumbnail = taskIdToBitmap[task.key.id], + reducedResolution = !highResEnabled, + ) + } + + fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0 + + fun preventThumbnailLoad(taskId: Int) { + completionPrevented.add(taskId) + } + + fun completeLoadingForTask(taskId: Int) { + completionPrevented.remove(taskId) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt new file mode 100644 index 000000000..765f0d1df --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt @@ -0,0 +1,31 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.util.TaskVisualsChangeListener + +class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier { + val listeners = mutableListOf() + + override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.add(listener) + } + + override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.remove(listener) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt index e160627d7..35af29f2c 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt @@ -16,25 +16,55 @@ package com.android.quickstep.recents.data +import android.graphics.drawable.Drawable import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class FakeTasksRepository : RecentTasksRepository { private var thumbnailDataMap: Map = emptyMap() + private var taskIconDataMap: Map = emptyMap() private var tasks: MutableStateFlow> = MutableStateFlow(emptyList()) - private var visibleTasks: MutableStateFlow> = MutableStateFlow(emptyList()) + private var visibleTasks: MutableStateFlow> = MutableStateFlow(emptySet()) override fun getAllTaskData(forceRefresh: Boolean): Flow> = tasks override fun getTaskDataById(taskId: Int): Flow = - getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + combine(getAllTaskData(), visibleTasks) { taskList, visibleTasks -> + taskList.filter { visibleTasks.contains(it.key.id) } + } + .map { taskList -> + val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null + Task(task).apply { + thumbnail = task.thumbnail + icon = task.icon + titleDescription = task.titleDescription + title = task.title + } + } - override fun setVisibleTasks(visibleTaskIdList: List) { + override fun getThumbnailById(taskId: Int): Flow = + getTaskDataById(taskId).map { it?.thumbnail } + + override fun getCurrentThumbnailById(taskId: Int): ThumbnailData? = + tasks.value.firstOrNull { it.key.id == taskId }?.thumbnail + + override fun setVisibleTasks(visibleTaskIdList: Set) { visibleTasks.value = visibleTaskIdList - tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } } + tasks.value = + tasks.value.map { + it.apply { + thumbnail = thumbnailDataMap[it.key.id] + taskIconDataMap[it.key.id]?.let { data -> + title = data.title + titleDescription = data.titleDescription + icon = data.icon + } + } + } } fun seedTasks(tasks: List) { @@ -44,4 +74,15 @@ class FakeTasksRepository : RecentTasksRepository { fun seedThumbnailData(thumbnailDataMap: Map) { this.thumbnailDataMap = thumbnailDataMap } + + fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) { + val iconData = FakeIconData(icon, contentDescription, title) + this.taskIconDataMap = mapOf(id to iconData) + } + + private data class FakeIconData( + val icon: Drawable, + val titleDescription: String, + val title: String, + ) } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt new file mode 100644 index 000000000..017f037fe --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt @@ -0,0 +1,47 @@ +/* + * 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.quickstep.recents.data + +import android.view.Surface.ROTATION_270 +import android.view.Surface.ROTATION_90 +import com.android.quickstep.orientation.SeascapePagedViewHandler +import com.android.quickstep.util.RecentsOrientedState +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [RecentsRotationStateRepositoryImpl] */ +class RecentsRotationStateRepositoryImplTest { + private val recentsOrientedState = mock() + + private val systemUnderTest = RecentsRotationStateRepositoryImpl(recentsOrientedState) + + @Test + fun orientedStateMappedCorrectly() { + whenever(recentsOrientedState.recentsActivityRotation).thenReturn(ROTATION_90) + whenever(recentsOrientedState.orientationHandler).thenReturn(SeascapePagedViewHandler()) + + assertThat(systemUnderTest.getRecentsRotationState()) + .isEqualTo( + RecentsRotationState( + activityRotation = ROTATION_90, + orientationHandlerRotation = ROTATION_270 + ) + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt new file mode 100644 index 000000000..b91f8bd6f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt @@ -0,0 +1,213 @@ +/* + * 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.quickstep.recents.data + +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions + +@RunWith(AndroidJUnit4::class) +class TaskVisualsChangedDelegateTest { + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + + val systemUnderTest = + TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier) + + @Test + fun addingFirstListener_addsListenerToNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun addingAndRemovingListener_removesListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners).isEmpty() + assertThat(highResLoadingStateNotifier.listeners).isEmpty() + } + + @Test + fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener) + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener) + + systemUnderTest.onTaskIconChanged(1) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val listener = mock() + // Correct match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + expectedListener, + ) + // 1 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1), + listener, + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2), + listener, + ) + // 0 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2), + listener, + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(listener) + } + + @Test + fun replacedTaskIconChangedCallbacks_notCalled() { + val replacedListener = mock() + val newListener = mock() + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + replacedListener, + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + newListener, + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verifyNoMoreInteractions(replacedListener) + verify(newListener).onTaskIconChanged() + } + + @Test + fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData) + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onHighResLoadingStateChanged_toEnabled_notifiesAllListeners() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onHighResLoadingStateChanged(true) + + verify(expectedListener).onHighResLoadingStateChanged(true) + verify(additionalListener).onHighResLoadingStateChanged(true) + } + + @Test + fun onHighResLoadingStateChanged_toDisabled_notifiesAllListeners() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onHighResLoadingStateChanged(false) + + verify(expectedListener).onHighResLoadingStateChanged(false) + verify(additionalListener).onHighResLoadingStateChanged(false) + } + + @Test + fun replacedTaskThumbnailChangedCallbacks_notCalled() { + val replacedListener1 = mock() + val newListener1 = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + replacedListener1, + ) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verifyNoMoreInteractions(replacedListener1) + verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData) + } + + private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) = + TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0) + + private companion object { + const val PACKAGE_NAME = "com.test.test" + const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2" + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt index c28a85a8f..e22892c9c 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt @@ -18,133 +18,455 @@ package com.android.quickstep.recents.data import android.content.ComponentName import android.content.Intent -import com.android.quickstep.TaskIconCache +import android.graphics.Bitmap +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TestDispatcherProvider import com.android.quickstep.util.DesktopTask -import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitTask import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) class TasksRepositoryTest { private val tasks = (0..5).map(::createTaskWithId) private val defaultTaskList = listOf( - GroupTask(tasks[0]), - GroupTask(tasks[1], tasks[2], null), - DesktopTask(tasks.subList(3, 6)) + SingleTask(tasks[0]), + SplitTask( + tasks[1], + tasks[2], + SplitConfigurationOptions.SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ -1, + /* rightBottomTaskId = */ -1, + /* snapPosition = */ SNAP_TO_2_50_50, + ), + ), + DesktopTask(deskId = 0, DEFAULT_DISPLAY, tasks.subList(3, 6)), ) private val recentsModel = FakeRecentTasksDataSource() private val taskThumbnailDataSource = FakeTaskThumbnailDataSource() - private val taskIconCache = mock() + private val taskIconDataSource = FakeTaskIconDataSource() + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + private val taskVisualsChangedDelegate = + spy(TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)) + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(dispatcher) private val systemUnderTest = - TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache) + TasksRepository( + recentsModel, + taskThumbnailDataSource, + taskIconDataSource, + taskVisualsChangedDelegate, + testScope.backgroundScope, + TestDispatcherProvider(dispatcher), + ) @Test - fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest { - recentsModel.seedTasks(defaultTaskList) - - assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks) - } - - @Test - fun getTaskDataByIdReturnsSpecificTask() = runTest { - recentsModel.seedTasks(defaultTaskList) - systemUnderTest.getAllTaskData(forceRefresh = true) - - assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) - } - - @Test - fun setVisibleTasksPopulatesThumbnails() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap1) - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - } - - @Test - fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - - // Prevent new loading of Bitmaps - taskThumbnailDataSource.shouldLoadSynchronously = false - systemUnderTest.setVisibleTasks(listOf(2, 3)) - - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - } - - @Test - fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - - // Prevent new loading of Bitmaps - taskThumbnailDataSource.shouldLoadSynchronously = false - systemUnderTest.setVisibleTasks(listOf(0, 1)) - - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull() - } - - @Test - fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest { - // Setup fakes - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - taskThumbnailDataSource.shouldLoadSynchronously = false - - // Setup TasksRepository - systemUnderTest.getAllTaskData(forceRefresh = true) - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // Assert there is no bitmap in first emission - val taskFlow = systemUnderTest.getTaskDataById(2) - val taskFlowValuesList = mutableListOf() - backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - taskFlow.toList(taskFlowValuesList) + fun getAllTaskDataReturnsFlattenedListOfTasks() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks) } - assertThat(taskFlowValuesList[0]!!.thumbnail).isNull() - // Simulate bitmap loading after first emission - taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke() + @Test + fun getTaskDataByIdReturnsSpecificTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) - // Check for second emission - assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2) - } + assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) + } + + @Test + fun getThumbnailByIdReturnsNullWithNoLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + assertThat(systemUnderTest.getThumbnailById(1).first()).isNull() + } + + @Test + fun getCurrentThumbnailByIdReturnsNullWithNoLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + assertThat(systemUnderTest.getCurrentThumbnailById(1)).isNull() + } + + @Test + fun getThumbnailByIdReturnsThumbnailWithLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(setOf(1)) + + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun whenThumbnailIsLoaded_getAllTaskData_usesPreviousLoadedThumbnailAndIcon() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(setOf(1)) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + + systemUnderTest.getAllTaskData(forceRefresh = true) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun getAllTaskData_clearsPreviouslyLoadedImagesForRemovedTasks() = + testScope.runTest { + // Setup data + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + // Load images for task 1 + systemUnderTest.setVisibleTasks(setOf(1)) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + + // Remove task 1 from "all data" + recentsModel.seedTasks( + defaultTaskList.filterNot { groupTask -> groupTask.tasks.any { it.key.id == 1 } } + ) + systemUnderTest.getAllTaskData(forceRefresh = true) + + // Assert task 1 was fully removed + assertThat(systemUnderTest.getThumbnailById(1).first()?.thumbnail).isNull() + verify(taskVisualsChangedDelegate).unregisterTaskThumbnailChangedCallback(tasks[1].key) + } + + @Test + fun getCurrentThumbnailByIdReturnsThumbnailWithLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(setOf(1)) + + assertThat(systemUnderTest.getCurrentThumbnailById(1)?.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun setVisibleTasksPopulatesThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + + assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap1) + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun setVisibleTasksPopulatesIcons() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + + systemUnderTest + .getTaskDataById(1) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.preventThumbnailLoad(2) + systemUnderTest.setVisibleTasks(setOf(2, 3)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedIcons() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + + // Prevent new loading of Drawables + taskIconDataSource.preventIconLoad(2) + systemUnderTest.setVisibleTasks(setOf(2, 3)) + + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + } + + @Test + fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + + val task2 = systemUnderTest.getTaskDataById(2).first()!! + assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2) + task2.assertHasIconDataFromSource(taskIconDataSource) + + systemUnderTest.setVisibleTasks(setOf(0, 1)) + + val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!! + assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull() + assertThat(task2AfterVisibleTasksChanged.icon).isNull() + assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull() + assertThat(task2AfterVisibleTasksChanged.title).isNull() + } + + @Test + fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = + testScope.runTest { + // Setup fakes + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + + // Setup TasksRepository + systemUnderTest.getAllTaskData(forceRefresh = true) + + val task2DataFlow = systemUnderTest.getTaskDataById(2) + val task2BitmapValues = mutableListOf() + testScope.backgroundScope.launch { + task2DataFlow.map { it?.thumbnail?.thumbnail }.toList(task2BitmapValues) + } + + // Check for first emission + assertThat(task2BitmapValues.single()).isNull() + + systemUnderTest.setVisibleTasks(setOf(2)) + // Check for second emission + assertThat(task2BitmapValues).isEqualTo(listOf(null, bitmap2)) + } + + @Test + fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + systemUnderTest.setVisibleTasks(setOf(1)) + + val expectedThumbnailData = createThumbnailData() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData) + + assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData) + } + + @Test + fun onHighResLoadingStateChanged_highResReplacesLowResThumbnail() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = false + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskThumbnailDataSource.highResEnabled = true + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + val firstThumbnailValue = task1ThumbnailValues.first()!! + assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(firstThumbnailValue.reducedResolution).isTrue() + + val lastThumbnailValue = task1ThumbnailValues.last()!! + assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedBitmap) + assertThat(lastThumbnailValue.reducedResolution).isFalse() + } + + @Test + fun onHighResLoadingStateChanged_invisibleTaskIgnored() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = false + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1)) + + val invisibleTaskId = 2 + val taskDataFlow = systemUnderTest.getTaskDataById(invisibleTaskId) + + val task2ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task2ThumbnailValues) + } + + taskThumbnailDataSource.highResEnabled = true + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + assertThat(task2ThumbnailValues.filterNotNull()).isEmpty() + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(2)).isEqualTo(0) + } + + @Test + fun onHighResLoadingStateChanged_lowResDoesNotReplaceHighResThumbnail() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = true + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskThumbnailDataSource.highResEnabled = false + taskVisualsChangedDelegate.onHighResLoadingStateChanged(false) + + val firstThumbnailValue = task1ThumbnailValues.first()!! + assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(firstThumbnailValue.reducedResolution).isFalse() + + val lastThumbnailValue = task1ThumbnailValues.last()!! + assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(lastThumbnailValue.reducedResolution).isFalse() + } + + @Test + fun onTaskIconChanged_setsNewIconOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(setOf(1)) + + val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable() + val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1IconValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.icon }.toList(task1IconValues) + } + taskIconDataSource.taskIdToDrawable[1] = expectedIcon + taskVisualsChangedDelegate.onTaskIconChanged(1) + + assertThat(task1IconValues.first()).isEqualTo(expectedPreviousIcon) + assertThat(task1IconValues.last()).isEqualTo(expectedIcon) + } + + @Test + fun setVisibleTasks_multipleTimesWithDifferentTasks_reusesThumbnailRequests() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + val taskDataFlow = systemUnderTest.getTaskDataById(1) + val task1IconValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.icon }.toList(task1IconValues) + } + + systemUnderTest.setVisibleTasks(setOf(1)) + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1) + + systemUnderTest.setVisibleTasks(setOf(1, 2)) + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1) + } private fun createTaskWithId(taskId: Int) = Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)) + + private fun createThumbnailData(): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt new file mode 100644 index 000000000..d3842562d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV +import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class GetSysUiStatusNavFlagsUseCaseTest { + private val sut: GetSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase() + + @Test + fun onLightStatusBarAppearance_returns_LightTheme() { + val thumbnailData = ThumbnailData(appearance = APPEARANCE_LIGHT_STATUS_BARS) + val flag = sut.invoke(thumbnailData) // 6 + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + } + + @Test + fun onLightNavBarsAppearance_returns_LightTheme() { + val thumbnailData = ThumbnailData(appearance = APPEARANCE_LIGHT_NAVIGATION_BARS) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onLightStatusBarAndNavBarAppearance_returns_LightTheme() { + val thumbnailData = + ThumbnailData( + appearance = APPEARANCE_LIGHT_STATUS_BARS or APPEARANCE_LIGHT_NAVIGATION_BARS + ) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onLightAppearance_returns_LightTheme() { + val thumbnailData = + ThumbnailData( + appearance = + APPEARANCE_LIGHT_CAPTION_BARS or + APPEARANCE_LIGHT_STATUS_BARS or + APPEARANCE_LIGHT_NAVIGATION_BARS + ) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onDarkAppearance_returns_DarkTheme() { + val thumbnailData = ThumbnailData(appearance = 0) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + } + + @Test + fun onUnrelatedDarkAppearance_returns_DarkTheme() { + val thumbnailData = ThumbnailData(appearance = 1) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + } + + @Test + fun whenThumbnailIsNull_returns_default() { + val flag = sut.invoke(null) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + private fun Int.assertContainsFlag(flag: Int) { + assertThat(this and flag).isNotEqualTo(0) + } + + private fun Int.assertDoesNotContainsFlag(flag: Int) { + assertThat(this and flag).isEqualTo(0) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt new file mode 100644 index 000000000..b036bceea --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.domain.model.TaskModel +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class GetTaskUseCaseTest { + private val unconfinedTestDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(unconfinedTestDispatcher) + + private val tasksRepository = FakeTasksRepository() + private val sut = GetTaskUseCase(repository = tasksRepository) + + @Before + fun setUp() { + tasksRepository.seedTasks(listOf(TASK_1)) + } + + @Test + fun taskNotSeeded_returnsNull() = + testScope.runTest { + val result = sut.invoke(NOT_FOUND_TASK_ID).firstOrNull() + assertThat(result).isNull() + } + + @Test + fun taskNotVisible_returnsNull() = + testScope.runTest { + val result = sut.invoke(TASK_1_ID).firstOrNull() + assertThat(result).isNull() + } + + @Test + fun taskVisible_returnsData() = + testScope.runTest { + tasksRepository.setVisibleTasks(setOf(TASK_1_ID)) + val expectedResult = + TaskModel( + id = TASK_1_ID, + title = "Title $TASK_1_ID", + titleDescription = "Content Description $TASK_1_ID", + icon = TASK_1_ICON, + thumbnail = null, + backgroundColor = Color.BLACK, + isLocked = false, + ) + val result = sut.invoke(TASK_1_ID).firstOrNull() + assertThat(result).isEqualTo(expectedResult) + } + + private companion object { + const val NOT_FOUND_TASK_ID = 404 + private const val TASK_1_ID = 1 + private val TASK_1_ICON = ShapeDrawable() + private val TASK_1 = + Task( + Task.TaskKey( + /* id = */ TASK_1_ID, + /* windowingMode = */ 0, + /* intent = */ Intent(), + /* sourceComponent = */ ComponentName("", ""), + /* userId = */ 0, + /* lastActiveTime = */ 2000, + ) + ) + .apply { + title = "Title 1" + titleDescription = "Content Description 1" + colorBackground = Color.BLACK + icon = TASK_1_ICON + thumbnail = null + isLocked = false + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt new file mode 100644 index 000000000..7646e6911 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Rect +import android.view.Surface.ROTATION_90 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory +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.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Test for [GetThumbnailPositionUseCase] */ +@RunWith(AndroidJUnit4::class) +class GetThumbnailPositionUseCaseTest { + private val deviceProfileRepository = FakeRecentsDeviceProfileRepository() + private val rotationStateRepository = FakeRecentsRotationStateRepository() + private val previewPositionHelperFactoryMock = mock() + private val previewPositionHelper = mock() + + private val systemUnderTest = + GetThumbnailPositionUseCase( + deviceProfileRepository = deviceProfileRepository, + rotationStateRepository = rotationStateRepository, + previewPositionHelperFactory = previewPositionHelperFactoryMock, + ) + + @Before + fun setUp() { + whenever(previewPositionHelperFactoryMock.create()).thenReturn(previewPositionHelper) + } + + @Test + fun nullThumbnailData_returnsIdentityMatrix() = runTest { + val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false) + val result = systemUnderTest.invoke(null, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun withoutThumbnail_returnsIdentityMatrix() = runTest { + val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false) + val result = + systemUnderTest.invoke(ThumbnailData(), CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest { + val isLargeScreen = true + deviceProfileRepository.setRecentsDeviceProfile( + deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen) + ) + val activityRotation = ROTATION_90 + rotationStateRepository.setRecentsRotationState( + rotationStateRepository + .getRecentsRotationState() + .copy(activityRotation = activityRotation) + ) + val isRtl = true + val isRotated = true + + whenever(previewPositionHelper.matrix).thenReturn(MATRIX) + whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated) + + val result = systemUnderTest.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl) + val expectedResult = ThumbnailPosition(MATRIX, isRotated) + assertThat(result).isEqualTo(expectedResult) + + verify(previewPositionHelper) + .updateThumbnailMatrix( + Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), + THUMBNAIL_DATA, + CANVAS_WIDTH, + CANVAS_HEIGHT, + isLargeScreen, + activityRotation, + isRtl, + ) + } + + @Test + fun multipleInvocations_usesPreviewPositionHelperFactoryEachTime() = runTest { + whenever(previewPositionHelper.matrix).thenReturn(MATRIX) + + val sut = + GetThumbnailPositionUseCase( + deviceProfileRepository = deviceProfileRepository, + rotationStateRepository = rotationStateRepository, + previewPositionHelperFactory = previewPositionHelperFactoryMock, + ) + verify(previewPositionHelperFactoryMock, times(0)).create() + + sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ true) + sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ false) + + // Each invocation of use case should use a fresh position helper acquired by the factory. + verify(previewPositionHelperFactoryMock, times(2)).create() + } + + private companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + const val CANVAS_WIDTH = 300 + const val CANVAS_HEIGHT = 600 + val MATRIX = + Matrix().apply { + setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f)) + } + + val THUMBNAIL_DATA = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + } + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt new file mode 100644 index 000000000..e8bca937b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Bitmap +import android.view.Surface +import android.view.Surface.ROTATION_90 +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class IsThumbnailValidUseCaseTest { + private val recentsRotationStateRepository = FakeRecentsRotationStateRepository() + private val systemUnderTest = IsThumbnailValidUseCase(recentsRotationStateRepository) + + @Test + fun withNullThumbnail_returnsInvalid() = runTest { + val isThumbnailValid = systemUnderTest(thumbnailData = null, viewWidth = 0, viewHeight = 0) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun sameAspectRatio_sameRotation_returnsValid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(), + viewWidth = THUMBNAIL_WIDTH * 2, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(true) + } + + @Test + fun differentAspectRatio_sameRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(), + viewWidth = THUMBNAIL_WIDTH, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun sameAspectRatio_differentRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(rotation = ROTATION_90), + viewWidth = THUMBNAIL_WIDTH * 2, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun differentAspectRatio_differentRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(rotation = ROTATION_90), + viewWidth = THUMBNAIL_WIDTH, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + private fun createThumbnailData( + rotation: Int = Surface.ROTATION_0, + width: Int = THUMBNAIL_WIDTH, + height: Int = THUMBNAIL_HEIGHT, + ): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(width) + whenever(bitmap.height).thenReturn(height) + return ThumbnailData(thumbnail = bitmap, rotation = rotation) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt new file mode 100644 index 000000000..7ca194afc --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.mapper + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.platform.test.annotations.EnableFlags +import android.view.Surface +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.Flags +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TaskUiStateMapperTest { + + @Test + fun taskData_isNull_returns_Uninitialized() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = null, + isLiveTile = false, + hasHeader = false, + clickCloseListener = null, + ) + assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized) + } + + @Test + fun taskData_isLiveTile_returns_LiveTile() { + val inputs = + listOf(TASK_DATA, TASK_DATA.copy(thumbnailData = null), TASK_DATA.copy(isLocked = true)) + inputs.forEach { input -> + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = input, + isLiveTile = true, + hasHeader = false, + clickCloseListener = null, + ) + assertThat(result).isEqualTo(LiveTile.WithoutHeader) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_isLiveTileWithHeader_returns_LiveTileWithHeader() { + val inputs = + listOf( + TASK_DATA, + TASK_DATA.copy(thumbnailData = null), + TASK_DATA.copy(isLocked = true), + TASK_DATA.copy(title = null), + ) + val closeCallback = View.OnClickListener {} + val expected = + LiveTile.WithHeader( + header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback) + ) + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = taskData, + isLiveTile = true, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_isLiveTileWithHeader_missingHeaderData_returns_LiveTileWithoutHeader() { + val inputs = + listOf( + TASK_DATA.copy(icon = null), + TASK_DATA.copy(titleDescription = null), + TASK_DATA.copy(icon = null, titleDescription = null), + ) + + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = taskData, + isLiveTile = true, + hasHeader = true, + clickCloseListener = {}, + ) + assertThat(result).isEqualTo(LiveTile.WithoutHeader) + } + } + + @Test + fun taskData_isStaticTile_returns_SnapshotSplash() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = TASK_DATA, + isLiveTile = false, + hasHeader = false, + clickCloseListener = null, + ) + + val expected = + TaskThumbnailUiState.SnapshotSplash( + snapshot = + Snapshot.WithoutHeader( + backgroundColor = TASK_BACKGROUND_COLOR, + bitmap = TASK_THUMBNAIL, + thumbnailRotation = Surface.ROTATION_0, + ), + splash = TASK_ICON, + ) + + assertThat(result).isEqualTo(expected) + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_isStaticTile_withHeader_returns_SnapshotSplashWithHeader() { + val inputs = listOf(TASK_DATA, TASK_DATA.copy(title = null)) + val closeCallback = View.OnClickListener {} + val expected = + TaskThumbnailUiState.SnapshotSplash( + snapshot = + Snapshot.WithHeader( + backgroundColor = TASK_BACKGROUND_COLOR, + bitmap = TASK_THUMBNAIL, + thumbnailRotation = Surface.ROTATION_0, + header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback), + ), + splash = TASK_ICON, + ) + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = taskData, + isLiveTile = false, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_isStaticTile_missingHeaderData_returns_SnapshotSplashWithoutHeader() { + val inputs = + listOf( + TASK_DATA.copy(titleDescription = null, icon = null), + TASK_DATA.copy(titleDescription = null), + TASK_DATA.copy(icon = null), + ) + val expected = + Snapshot.WithoutHeader( + backgroundColor = TASK_BACKGROUND_COLOR, + thumbnailRotation = Surface.ROTATION_0, + bitmap = TASK_THUMBNAIL, + ) + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = taskData, + isLiveTile = false, + hasHeader = true, + clickCloseListener = {}, + ) + + assertThat(result).isInstanceOf(TaskThumbnailUiState.SnapshotSplash::class.java) + result as TaskThumbnailUiState.SnapshotSplash + assertThat(result.snapshot).isEqualTo(expected) + } + } + + @Test + fun taskData_thumbnailIsNull_returns_BackgroundOnly() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = TASK_DATA.copy(thumbnailData = null), + isLiveTile = false, + hasHeader = false, + clickCloseListener = null, + ) + + val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) + assertThat(result).isEqualTo(expected) + } + + @Test + fun taskData_isLocked_returns_BackgroundOnly() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = TASK_DATA.copy(isLocked = true), + isLiveTile = false, + hasHeader = false, + clickCloseListener = null, + ) + + val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) + assertThat(result).isEqualTo(expected) + } + + private companion object { + const val TASK_TITLE_DESCRIPTION = "Title Description 1" + var TASK_ID = 1 + val TASK_ICON = ShapeDrawable() + val TASK_THUMBNAIL = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val TASK_THUMBNAIL_DATA = + ThumbnailData(thumbnail = TASK_THUMBNAIL, rotation = Surface.ROTATION_0) + val TASK_BACKGROUND_COLOR = Color.rgb(1, 2, 3) + val TASK_DATA = + TaskData.Data( + TASK_ID, + title = "Task 1", + titleDescription = TASK_TITLE_DESCRIPTION, + icon = TASK_ICON, + thumbnailData = TASK_THUMBNAIL_DATA, + backgroundColor = TASK_BACKGROUND_COLOR, + isLocked = false, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt new file mode 100644 index 000000000..333c2856e --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.viewmodel + +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS +import com.android.launcher3.util.TestDispatcherProvider +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.quickstep.recents.domain.model.TaskModel +import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase +import com.android.quickstep.recents.domain.usecase.GetTaskUseCase +import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class TaskViewModelTest { + private val unconfinedTestDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(unconfinedTestDispatcher) + + private val recentsViewData = RecentsViewData() + private val getTaskUseCase = mock() + private val getThumbnailPositionUseCase = mock() + private val isThumbnailValidUseCase = + spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository())) + private lateinit var sut: TaskViewModel + + @Before + fun setUp() { + sut = createTaskViewModel(TaskViewType.SINGLE) + whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) }) + whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) }) + whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) }) + whenever(getTaskUseCase.invoke(INVALID_TASK_ID)).thenReturn(flow { emit(null) }) + recentsViewData.runningTaskIds.value = emptySet() + } + + @Test + fun singleTaskRetrieved_when_validTaskId() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + val expectedResult = + TaskTileUiState( + tasks = listOf(TASK_MODEL_1.toUiState()), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun hasHeader_when_taskViewTypeIsDesktop() = + testScope.runTest { + val expectedResults = + mapOf( + TaskViewType.SINGLE to false, + TaskViewType.GROUPED to false, + TaskViewType.DESKTOP to true, + ) + + expectedResults.forEach { (type, expectedResult) -> + sut = + TaskViewModel( + taskViewType = type, + recentsViewData = recentsViewData, + getTaskUseCase = getTaskUseCase, + getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(), + isThumbnailValidUseCase = isThumbnailValidUseCase, + getThumbnailPositionUseCase = getThumbnailPositionUseCase, + dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher), + ) + sut.bind(TASK_MODEL_1.id) + assertThat(sut.state.first().hasHeader).isEqualTo(expectedResult) + } + } + + @Test + fun multipleTasksRetrieved_when_validTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, INVALID_TASK_ID) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + TaskData.NoData(INVALID_TASK_ID), + ), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isLiveTile_when_runningTasksMatchTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + ), + isLiveTile = true, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTaskShowScreenshotIsTrue() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = true + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + ), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTasksMatchPartialTasks_lessRunningTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = setOf(TASK_MODEL_1.id, TASK_MODEL_2.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + ), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTasksMatchPartialTasks_moreRunningTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + val expectedResult = + TaskTileUiState( + tasks = listOf(TASK_MODEL_1.toUiState(), TASK_MODEL_2.toUiState()), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun noDataAvailable_when_InvalidTaskId() = + testScope.runTest { + sut.bind(INVALID_TASK_ID) + val expectedResult = + TaskTileUiState( + listOf(TaskData.NoData(INVALID_TASK_ID)), + isLiveTile = false, + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_DEFAULT, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun taskOverlayEnabled_when_OverlayIsEnabledForVisibleSingleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isTrue() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsEnabledForInvisibleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(2) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsDisabledForVisibleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = false + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun isCentralTask_when_CentralTaskIdsMatchTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + recentsViewData.centralTaskIds.value = setOf(TASK_MODEL_1.id, TASK_MODEL_2.id) + + assertThat(sut.state.first().isCentralTask).isTrue() + } + + @Test + fun isNotCentralTask_when_CentralTaskIdsDoMatchTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + recentsViewData.centralTaskIds.value = setOf(TASK_MODEL_3.id) + + assertThat(sut.state.first().isCentralTask).isFalse() + } + + @Test + fun shouldShowSplash_calls_useCase() { + sut.isThumbnailValid(null, 0, 0) + verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt()) + } + + private fun TaskModel.toUiState() = + TaskData.Data( + taskId = id, + title = title, + titleDescription = titleDescription, + icon = icon!!, + thumbnailData = thumbnail, + backgroundColor = backgroundColor, + isLocked = isLocked, + ) + + private fun createTaskViewModel(taskViewType: TaskViewType) = + TaskViewModel( + taskViewType = taskViewType, + recentsViewData = recentsViewData, + getTaskUseCase = getTaskUseCase, + getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(), + isThumbnailValidUseCase = isThumbnailValidUseCase, + getThumbnailPositionUseCase = getThumbnailPositionUseCase, + dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher), + ) + + private companion object { + const val INVALID_TASK_ID = -1 + const val FLAGS_APPEARANCE_LIGHT_THEME = FLAG_LIGHT_STATUS or FLAG_LIGHT_NAV + const val FLAGS_APPEARANCE_DEFAULT = 0 + const val APPEARANCE_LIGHT_THEME = + APPEARANCE_LIGHT_CAPTION_BARS or + APPEARANCE_LIGHT_STATUS_BARS or + APPEARANCE_LIGHT_NAVIGATION_BARS + + val TASK_MODEL_1 = + TaskModel( + 1, + "Title 1", + "Content Description 1", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.BLACK, + false, + ) + val TASK_MODEL_2 = + TaskModel( + 2, + "Title 2", + "Content Description 2", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.RED, + true, + ) + val TASK_MODEL_3 = + TaskModel( + 3, + "Title 3", + "Content Description 3", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.BLUE, + false, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt new file mode 100644 index 000000000..a32e07ded --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt @@ -0,0 +1,138 @@ +/* + * 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.quickstep.recents.viewmodel + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.view.Surface +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class RecentsViewModelTest { + private val tasksRepository = FakeTasksRepository() + private val recentsViewData = RecentsViewData() + private val systemUnderTest = RecentsViewModel(tasksRepository, recentsViewData) + + private val tasks = (0..5).map(::createTaskWithId) + + @Test + fun taskVisibilityControlThumbnailsAvailability() = runTest { + val thumbnailData1 = createThumbnailData() + val thumbnailData2 = createThumbnailData() + tasksRepository.seedTasks(tasks) + tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2)) + + val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1) + val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2) + + systemUnderTest.refreshAllTaskData() + + assertThat(thumbnailDataFlow1.first()).isNull() + assertThat(thumbnailDataFlow2.first()).isNull() + + systemUnderTest.updateVisibleTasks(listOf(1, 2)) + + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) + + systemUnderTest.updateVisibleTasks(listOf(1)) + + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isNull() + + systemUnderTest.onReset() + + assertThat(thumbnailDataFlow1.first()).isNull() + assertThat(thumbnailDataFlow2.first()).isNull() + } + + @Test + fun updatesRunningTaskShowScreenshot() = runTest { + systemUnderTest.setRunningTaskShowScreenshot(true) + systemUnderTest.waitForRunningTaskShowScreenshotToUpdate() + } + + @Test + fun waitForThumbnailsToUpdate() = runTest { + // Given taskRepository with visible 2 tasks containing thumbnailData + val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 } + val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 } + tasksRepository.seedTasks(tasks) + tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2)) + systemUnderTest.updateVisibleTasks(listOf(1, 2)) + + val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1) + val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2) + + // Then getThumbnailById should initially contains correct thumbnailData + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) + + // When thumbnailData is updated in taskRepository + tasksRepository.seedThumbnailData( + mapOf(1 to thumbnailData1, 2 to createThumbnailData().apply { snapshotId = 3 }) + ) + // setVisibleTasks forces FakeTasksRepository to update the flows returned by + // getThumbnailById + tasksRepository.setVisibleTasks(setOf(1, 2)) + + // Then wait for thumbnailData should complete, and the previous getThumbnailById flow + // should return updated values + systemUnderTest.waitForThumbnailsToUpdate( + mapOf(2 to createThumbnailData().apply { snapshotId = 3 }) + ) + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3) + } + + @Test + fun waitForThumbnailsToUpdate_emptyMap() = runTest { + systemUnderTest.waitForThumbnailsToUpdate(emptyMap()) + } + + @Test + fun waitForThumbnailsToUpdate_null() = runTest { + systemUnderTest.waitForThumbnailsToUpdate(null) + } + + private fun createTaskWithId(taskId: Int) = + Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.argb(taskId, taskId, taskId, taskId) + } + + private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap, rotation = rotation) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt new file mode 100644 index 000000000..011967968 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt @@ -0,0 +1,118 @@ +/* + * 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.quickstep.recents.window + +import android.graphics.Point +import android.hardware.display.DisplayManager +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display +import android.view.DisplayAdjustments +import android.view.Surface +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW +import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW +import com.android.launcher3.util.LauncherModelHelper +import com.android.launcher3.util.window.CachedDisplayInfo +import com.android.quickstep.fallback.window.RecentsDisplayModel +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW) +class RecentsDisplayModelTest { + @get:Rule val setFlagsRule = SetFlagsRule() + + // initiate dagger components for injection + private val launcherModelHelper = LauncherModelHelper() + private val context = spy(launcherModelHelper.sandboxContext) + private val displayManager: DisplayManager = context.spyService(DisplayManager::class.java) + private val display: Display = mock() + + private lateinit var recentsDisplayModel: RecentsDisplayModel + + private val width = 2208 + private val height = 1840 + + @Before + fun setup() { + // Mock display + val displayInfo = CachedDisplayInfo(Point(width, height), Surface.ROTATION_0) + whenever(display.rotation).thenReturn(displayInfo.rotation) + whenever(display.displayAdjustments).thenReturn(DisplayAdjustments()) + whenever(context.display).thenReturn(display) + + // Mock displayManager + whenever(displayManager.getDisplay(anyInt())).thenReturn(display) + + runOnMainSync { recentsDisplayModel = RecentsDisplayModel.INSTANCE.get(context) } + } + + @Test + fun testEnsureSingleton() { + val recentsDisplayModel2 = RecentsDisplayModel.INSTANCE.get(context) + assert(recentsDisplayModel == recentsDisplayModel2) + } + + @Test + fun testDefaultDisplayCreation() { + Assert.assertNotNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY)) + Assert.assertNotNull( + recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY) + ) + } + + @Test + fun testCreateSeparateInstances() { + val displayId = Display.DEFAULT_DISPLAY + 1 + runOnMainSync { recentsDisplayModel.storeDisplayResource(displayId) } + + val defaultManager = recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY) + val secondaryManager = recentsDisplayModel.getRecentsWindowManager(displayId) + Assert.assertNotSame(defaultManager, secondaryManager) + + val defaultInterface = + recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY) + val secondInterface = recentsDisplayModel.getFallbackWindowInterface(displayId) + Assert.assertNotSame(defaultInterface, secondInterface) + } + + @Test + fun testDestroy() { + Assert.assertNotNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY)) + Assert.assertNotNull( + recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY) + ) + recentsDisplayModel.destroy() + Assert.assertNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY)) + Assert.assertNull(recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY)) + } + + private fun runOnMainSync(f: Runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt deleted file mode 100644 index 3b8754c20..000000000 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt +++ /dev/null @@ -1,178 +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.quickstep.task.thumbnail - -import android.content.ComponentName -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.Rect -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.quickstep.recents.data.FakeTasksRepository -import com.android.quickstep.recents.viewmodel.RecentsViewData -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized -import com.android.quickstep.task.viewmodel.TaskViewData -import com.android.systemui.shared.recents.model.Task -import com.android.systemui.shared.recents.model.ThumbnailData -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@RunWith(AndroidJUnit4::class) -class TaskThumbnailViewModelTest { - private val recentsViewData = RecentsViewData() - private val taskViewData = TaskViewData() - private val tasksRepository = FakeTasksRepository() - private val systemUnderTest = - TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository) - - private val tasks = (0..5).map(::createTaskWithId) - - @Test - fun initialStateIsUninitialized() = runTest { - assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) - } - - @Test - fun bindRunningTask_thenStateIs_LiveTile() = runTest { - tasksRepository.seedTasks(tasks) - val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true) - systemUnderTest.bind(taskThumbnail) - - assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) - } - - @Test - fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest { - recentsViewData.fullscreenProgress.value = 0.5f - - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f) - - recentsViewData.fullscreenProgress.value = 0.6f - - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f) - } - - @Test - fun setAncestorScales_thenScaleIsCalculated() = runTest { - recentsViewData.scale.value = 0.5f - taskViewData.scale.value = 0.6f - - assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f) - } - - @Test - fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() = - runTest { - tasksRepository.seedTasks(tasks) - val runningTask = TaskThumbnail(taskId = 1, isRunning = true) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) - systemUnderTest.bind(runningTask) - assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) - - systemUnderTest.bind(stoppedTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest { - tasksRepository.seedTasks(tasks) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(stoppedTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest { - tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData())) - tasks[2].isLocked = true - tasksRepository.seedTasks(tasks) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest { - val expectedThumbnailData = createThumbnailData() - tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData)) - tasksRepository.seedTasks(tasks) - tasksRepository.setVisibleTasks(listOf(2)) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) - ) - ) - } - - @Test - fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest { - val expectedThumbnailData = createThumbnailData() - tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData)) - tasksRepository.seedTasks(tasks) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - tasksRepository.setVisibleTasks(listOf(2)) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) - ) - ) - } - - private fun createTaskWithId(taskId: Int) = - Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - colorBackground = Color.argb(taskId, taskId, taskId, taskId) - } - - private fun createThumbnailData(): ThumbnailData { - val bitmap = mock() - whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) - whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) - - return ThumbnailData(thumbnail = bitmap) - } - - companion object { - const val THUMBNAIL_WIDTH = 100 - const val THUMBNAIL_HEIGHT = 200 - } -} diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt similarity index 94% rename from quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt index 4d10f0f51..26418d876 100644 --- a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt @@ -55,11 +55,10 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { private val taskbarDragLayer = mock() private val taskbarSharedState = mock() private var isInDesktopMode = false - private val isInDesktopModeProvider = { isInDesktopMode } private val launcherPrefs = - mock { - on { get(TASKBAR_PINNING) } doReturn false - on { get(TASKBAR_PINNING_IN_DESKTOP_MODE) } doReturn false + mock().apply { + doReturn(false).whenever(this).get(TASKBAR_PINNING) + doReturn(false).whenever(this).get(TASKBAR_PINNING_IN_DESKTOP_MODE) } private val statsLogger = mock() private val statsLogManager = mock { on { logger() } doReturn statsLogger } @@ -71,8 +70,13 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs) whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer) whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager) - pinningController = - spy(TaskbarPinningController(taskbarActivityContext, isInDesktopModeProvider)) + whenever( + taskbarControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + taskbarActivityContext.displayId + ) + ) + .thenAnswer { _ -> isInDesktopMode } + pinningController = spy(TaskbarPinningController(taskbarActivityContext)) pinningController.init(taskbarControllers, taskbarSharedState) } diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt similarity index 83% rename from quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt index b637e7d4c..d66197a6f 100644 --- a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt @@ -16,6 +16,7 @@ package com.android.quickstep.taskbar.customization +import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator import com.android.launcher3.taskbar.customization.TaskbarIconSpecs import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator @@ -32,15 +33,26 @@ import org.mockito.kotlin.whenever class TaskbarSpecsEvaluatorTest { private val taskbarFeatureEvaluator = mock() - private val taskbarSpecsEvaluator = spy(TaskbarSpecsEvaluator(taskbarFeatureEvaluator)) + private val taskbarActivityContext = mock() + private var taskbarSpecsEvaluator = + spy(TaskbarSpecsEvaluator(taskbarActivityContext, taskbarFeatureEvaluator, 0, 0)) @Test - fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumn() { + fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInLandscape() { doReturn(true).whenever(taskbarFeatureEvaluator).isTransient - assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(6, 5)) + doReturn(true).whenever(taskbarFeatureEvaluator).isLandscape + assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4)) .isEqualTo(TaskbarIconSpecs.iconSize52dp) } + @Test + fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInPortrait() { + doReturn(true).whenever(taskbarFeatureEvaluator).isTransient + doReturn(false).whenever(taskbarFeatureEvaluator).isLandscape + assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4)) + .isEqualTo(TaskbarIconSpecs.iconSize48dp) + } + @Test fun testGetIconSizeByGrid_whenTaskbarIsTransient_withInvalidRowAndColumn() { doReturn(true).whenever(taskbarFeatureEvaluator).isTransient diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt new file mode 100644 index 000000000..b4c236e99 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.hardware.input.InputManager +import android.view.InputDevice +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.IntArray +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.TestUtil +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class ActiveTrackpadListTest { + + @get:Rule val context = SandboxApplication() + + private val inputDeviceIds = IntArray() + private lateinit var inputManager: InputManager + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + inputManager = context.spyService(InputManager::class.java) + doAnswer { inputDeviceIds.toArray() }.whenever(inputManager).inputDeviceIds + + doReturn(null).whenever(inputManager).getInputDevice(eq(1)) + doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD)) + .whenever(inputManager) + .getInputDevice(eq(2)) + doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD)) + .whenever(inputManager) + .getInputDevice(eq(3)) + doReturn(mockDevice(SOURCE_MOUSE)).whenever(inputManager).getInputDevice(eq(4)) + } + + @Test + fun `initialize correct devices`() { + inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4)) + + val list = ActiveTrackpadList(context) {} + assertEquals(2, list.size()) + assertTrue(list.contains(2)) + assertTrue(list.contains(3)) + } + + @Test + fun `update callback not called in constructor`() { + inputDeviceIds.addAll(IntArray.wrap(2, 3)) + + var updateCalled = false + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + + assertEquals(2, list.size()) + assertFalse(updateCalled) + } + + @Test + fun `update called on add only once`() { + var updateCalled = false + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + + assertFalse(updateCalled) + assertEquals(0, list.size()) + + list.onInputDeviceAdded(1) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertFalse(updateCalled) + assertEquals(0, list.size()) + + list.onInputDeviceAdded(2) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertTrue(updateCalled) + assertEquals(1, list.size()) + + updateCalled = false + list.onInputDeviceAdded(3) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertFalse(updateCalled) + assertEquals(2, list.size()) + } + + @Test + fun `update called on remove only once`() { + var updateCalled = false + inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4)) + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(2, list.size()) + + list.onInputDeviceRemoved(2) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(1, list.size()) + assertFalse(updateCalled) + + list.onInputDeviceRemoved(3) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(0, list.size()) + assertTrue(updateCalled) + } + + private fun mockDevice(sources: Int) = + mock(InputDevice::class.java).apply { doReturn(sources).whenever(this).sources } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt index ece67aff6..c325af4d3 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt @@ -16,7 +16,10 @@ package com.android.quickstep.util -import android.content.Context +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.res.Resources +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.logging.StatsLogManager @@ -24,13 +27,15 @@ import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.views.ActivityContext import com.android.quickstep.TopTaskTracker import com.android.quickstep.TopTaskTracker.CachedTaskInfo import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70 -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30 +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Before @@ -53,32 +58,34 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class AppPairsControllerTest { - @Mock lateinit var context: Context + @Mock lateinit var context: ActivityContext + @Mock lateinit var resources: Resources @Mock lateinit var splitSelectStateController: SplitSelectStateController @Mock lateinit var statsLogManager: StatsLogManager private lateinit var appPairsController: AppPairsController - private val left30: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_30_70) + private val left33: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_33_66) } private val left50: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_50_50) + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50) } - private val left70: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_70_30) + private val left66: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_66_33) } - private val right30: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_30_70) + private val right33: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_33_66) } private val right50: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_50_50) + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_50_50) } - private val right70: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30) + private val right66: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_66_33) } @Mock lateinit var mockAppPairIcon: AppPairIcon + @Mock lateinit var mockDisplay: Display @Mock lateinit var mockTaskbarActivityContext: TaskbarActivityContext @Mock lateinit var mockTopTaskTracker: TopTaskTracker @Mock lateinit var mockCachedTaskInfo: CachedTaskInfo @@ -101,38 +108,42 @@ class AppPairsControllerTest { // Stub methods on appPairsController so that they return mocks spyAppPairsController = spy(appPairsController) whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) + whenever(mockAppPairIcon.display).thenReturn(mockDisplay) + whenever(mockDisplay.displayId).thenReturn(DEFAULT_DISPLAY) doReturn(mockTopTaskTracker).whenever(spyAppPairsController).topTaskTracker - whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo) + whenever(mockTopTaskTracker.getCachedTopTask(any(), any())).thenReturn(mockCachedTaskInfo) whenever(mockTask1.getKey()).thenReturn(mockTaskKey1) whenever(mockTask2.getKey()).thenReturn(mockTaskKey2) doNothing().whenever(spyAppPairsController).launchAppPair(any(), any()) doNothing() .whenever(spyAppPairsController) .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + whenever(mockAppPairIcon.context.resources).thenReturn(resources) + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false) } @Test fun shouldEncodeRankCorrectly() { - assertEquals("left + 30-70 should encode as 0 (0b0)", 0, left30) + assertEquals("left + 33-66 should encode as 0 (0b0)", 0, left33) assertEquals("left + 50-50 should encode as 1 (0b1)", 1, left50) - assertEquals("left + 70-30 should encode as 2 (0b10)", 2, left70) + assertEquals("left + 66-33 should encode as 2 (0b10)", 2, left66) // See AppPairsController#BITMASK_SIZE and BITMASK_FOR_SNAP_POSITION for context - assertEquals("right + 30-70 should encode as 1 followed by 16 0s", 1 shl 16, right30) + assertEquals("right + 33-66 should encode as 1 followed by 16 0s", 1 shl 16, right33) assertEquals("right + 50-50 should encode as the above value + 1", (1 shl 16) + 1, right50) - assertEquals("right + 70-30 should encode as the above value + 2", (1 shl 16) + 2, right70) + assertEquals("right + 66-33 should encode as the above value + 2", (1 shl 16) + 2, right66) } @Test fun shouldDecodeRankCorrectly() { assertEquals( - "left + 30-70 should decode to left", + "left + 33-66 should decode to left", STAGE_POSITION_TOP_OR_LEFT, - AppPairsController.convertRankToStagePosition(left30), + AppPairsController.convertRankToStagePosition(left33), ) assertEquals( - "left + 30-70 should decode to 30-70", - SNAP_TO_30_70, - AppPairsController.convertRankToSnapPosition(left30), + "left + 33-66 should decode to 33-66", + SNAP_TO_2_33_66, + AppPairsController.convertRankToSnapPosition(left33), ) assertEquals( @@ -142,30 +153,30 @@ class AppPairsControllerTest { ) assertEquals( "left + 50-50 should decode to 50-50", - SNAP_TO_50_50, + SNAP_TO_2_50_50, AppPairsController.convertRankToSnapPosition(left50), ) assertEquals( - "left + 70-30 should decode to left", + "left + 66-33 should decode to left", STAGE_POSITION_TOP_OR_LEFT, - AppPairsController.convertRankToStagePosition(left70), + AppPairsController.convertRankToStagePosition(left66), ) assertEquals( - "left + 70-30 should decode to 70-30", - SNAP_TO_70_30, - AppPairsController.convertRankToSnapPosition(left70), + "left + 66-33 should decode to 66-33", + SNAP_TO_2_66_33, + AppPairsController.convertRankToSnapPosition(left66), ) assertEquals( - "right + 30-70 should decode to right", + "right + 33-66 should decode to right", STAGE_POSITION_BOTTOM_OR_RIGHT, - AppPairsController.convertRankToStagePosition(right30), + AppPairsController.convertRankToStagePosition(right33), ) assertEquals( - "right + 30-70 should decode to 30-70", - SNAP_TO_30_70, - AppPairsController.convertRankToSnapPosition(right30), + "right + 33-66 should decode to 33-66", + SNAP_TO_2_33_66, + AppPairsController.convertRankToSnapPosition(right33), ) assertEquals( @@ -175,19 +186,19 @@ class AppPairsControllerTest { ) assertEquals( "right + 50-50 should decode to 50-50", - SNAP_TO_50_50, + SNAP_TO_2_50_50, AppPairsController.convertRankToSnapPosition(right50), ) assertEquals( - "right + 70-30 should decode to right", + "right + 66-33 should decode to right", STAGE_POSITION_BOTTOM_OR_RIGHT, - AppPairsController.convertRankToStagePosition(right70), + AppPairsController.convertRankToStagePosition(right66), ) assertEquals( - "right + 70-30 should decode to 70-30", - SNAP_TO_70_30, - AppPairsController.convertRankToSnapPosition(right70), + "right + 66-33 should decode to 66-33", + SNAP_TO_2_66_33, + AppPairsController.convertRankToSnapPosition(right66), ) } @@ -202,7 +213,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -226,7 +237,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -250,7 +261,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -274,7 +285,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -298,7 +309,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -322,7 +333,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -341,12 +352,16 @@ class AppPairsControllerTest { whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 1 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(1) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(1) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -365,12 +380,16 @@ class AppPairsControllerTest { whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 2 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(2) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(2) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -383,18 +402,84 @@ class AppPairsControllerTest { .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT)) } + @Test + fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 1 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 1 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(1) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 2 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 2 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(2) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + @Test fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() { // Test launching apps 1 and 2 from app pair whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 3 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(3) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(3))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(3) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt new file mode 100644 index 000000000..296171b86 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.os.SystemClock +import android.view.MotionEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.util.function.Consumer +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class CachedEventDispatcherTest { + + private lateinit var underTest: CachedEventDispatcher + + @Mock private lateinit var consumer: Consumer + @Captor private lateinit var motionEventCaptor: ArgumentCaptor + + private lateinit var motionEvent: MotionEvent + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + motionEvent = + MotionEvent.obtain( + 0L, + SystemClock.elapsedRealtime(), + MotionEvent.ACTION_DOWN, + 0f, + 0f, + 0, + ) + underTest = CachedEventDispatcher() + } + + @Test + fun testInitialState() { + assertFalse(underTest.hasConsumer()) + } + + @Test + fun dispatchMotionEvents() { + underTest.setConsumer(consumer) + + underTest.dispatchEvent(motionEvent) + + assertTrue(underTest.hasConsumer()) + verify(consumer).accept(motionEventCaptor.capture()) + assertTrue(isMotionEventSame(motionEventCaptor.value, motionEvent)) + } + + @Test + fun dispatchMotionEvents_after_settingConsumer() { + underTest.dispatchEvent(motionEvent) + + underTest.setConsumer(consumer) + + verify(consumer).accept(motionEventCaptor.capture()) + assertTrue(isMotionEventSame(motionEventCaptor.value, motionEvent)) + } + + @Test + fun clearConsumer_notDispatchToConsumer() { + underTest.setConsumer(consumer) + underTest.dispatchEvent(motionEvent) + reset(consumer) + + underTest.clearConsumer() + + assertFalse(underTest.hasConsumer()) + underTest.dispatchEvent(motionEvent) + verifyNoMoreInteractions(consumer) + } + + private fun isMotionEventSame(e1: MotionEvent, e2: MotionEvent): Boolean { + return e1.action == e2.action && + e1.eventTime == e2.eventTime && + e1.x == e2.x && + e1.y == e2.y + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java new file mode 100644 index 000000000..61971b1fc --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java @@ -0,0 +1,336 @@ +/* + * 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.quickstep.util; + +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED; +import static com.android.quickstep.util.ContextualSearchInvoker.KEYGUARD_SHOWING_SYSUI_FLAGS; +import static com.android.quickstep.util.ContextualSearchInvoker.SHADE_EXPANDED_SYSUI_FLAGS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.contextualsearch.ContextualSearchManager; +import android.content.Context; +import android.content.pm.PackageManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.orientation.RecentsPagedOrientationHandler; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Robolectric unit tests for {@link ContextualSearchInvoker} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContextualSearchInvokerTest { + + private static final int CONTEXTUAL_SEARCH_ENTRY_POINT = 123; + + private @Mock PackageManager mMockPackageManager; + private @Mock ContextualSearchStateManager mMockStateManager; + private @Mock TopTaskTracker mMockTopTaskTracker; + private @Mock SystemUiProxy mMockSystemUiProxy; + private @Mock StatsLogManager mMockStatsLogManager; + private @Mock StatsLogManager.StatsLogger mMockStatsLogger; + private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager; + private @Mock ContextualSearchManager mMockContextualSearchManager; + private @Mock BaseContainerInterface mMockContainerInterface; + private @Mock RecentsViewContainer mMockRecentsViewContainer; + private @Mock RecentsView mMockRecentsView; + private @Mock RecentsPagedOrientationHandler mMockOrientationHandler; + private ContextualSearchInvoker mContextualSearchInvoker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true); + Context context = spy(getApplicationContext()); + doReturn(mMockPackageManager).when(context).getPackageManager(); + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{}); + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true); + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true); + when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger); + when(mMockContainerInterface.getCreatedContainer()).thenReturn(mMockRecentsViewContainer); + when(mMockRecentsViewContainer.getOverviewPanel()).thenReturn(mMockRecentsView); + + mContextualSearchInvoker = spy(new ContextualSearchInvoker(context, mMockStateManager, + mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager, + mMockContextualSearchHapticManager, mMockContextualSearchManager + )); + doReturn(mMockContainerInterface).when(mContextualSearchInvoker) + .getRecentsContainerInterface(); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchFeatureIsNotAvailable() { + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsAvailable() { + assertTrue("Expected invocation checks to succeed", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsNotAvailable() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_settingDisabled() { + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(false); + + assertFalse("Expected invocation checks to fail when setting is disabled", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when notification shade is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn( + KEYGUARD_SHOWING_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when keyguard is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_disallowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(false); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertFalse("Expected invocation checks to fail over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_allowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(true); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertTrue("Expected invocation checks to succeed over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isFakeLandscape() { + when(mMockRecentsView.getPagedOrientationHandler()).thenReturn(mMockOrientationHandler); + when(mMockOrientationHandler.isLayoutNaturalToLauncher()).thenReturn(false); + assertFalse("Expect invocation checks to fail in fake landscape.", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticDisabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticEnabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still vibrate based on the flag. + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticDisabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected ContextualSearch invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still don't vibrate based on the flag. + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + switchToScreenshotCaptor.getValue().run(); + verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + finishRecentsAnimationCaptor.getValue().run(); + verify(mMockContextualSearchManager).startContextualSearch(CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToSwitchToScreenshot() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + + // Don't run switchToScreenshot's callback. Therefore, recents animation should not finish. + verify(mMockRecentsView, never()).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + // And ContextualSearch should not start. + verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt()); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToFinishRecentsAnimation() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + switchToScreenshotCaptor.getValue().run(); + verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + // Don't run finishRecentsAnimation's callback. Therefore ContextualSearch should not start. + verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt()); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + private AutoCloseable overrideSearchHapticCommitFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_SEARCH_HAPTIC_COMMIT", + value, + () -> DeviceConfigWrapper.get().getEnableSearchHapticCommit()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt new file mode 100644 index 000000000..15da4d4de --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt @@ -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.quickstep.util + +import android.content.ComponentName +import android.content.Intent +import android.view.Display.DEFAULT_DISPLAY +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +class DesktopTaskTest { + + @Test + fun testDesktopTask_sameInstance_isEqual() { + val task = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testDesktopTask_identicalConstructor_isEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_copy_isEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_differentDeskIds_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 1, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_differentTaskIds_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_differentLength_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1, 2)) + assertThat(task1).isNotEqualTo(task2) + } + + private fun createTasks(vararg ids: Int): List { + return ids.map { Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 0)) } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt similarity index 90% rename from quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt index c190cfeba..555e62bea 100644 --- a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt @@ -18,11 +18,12 @@ package com.android.quickstep.util import android.graphics.Rect import android.graphics.Region -import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager +import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest import com.android.launcher3.util.Executors +import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.quickstep.util.GestureExclusionManager.ExclusionListener import org.junit.Before import org.junit.Test @@ -31,11 +32,12 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.reset import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions /** Unit test for [GestureExclusionManager]. */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) class GestureExclusionManagerTest { @Mock private lateinit var windowManager: IWindowManager @@ -72,7 +74,7 @@ class GestureExclusionManagerTest { underTest.addListener(listener2) awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyNoMoreInteractions(windowManager) } @Test @@ -98,7 +100,7 @@ class GestureExclusionManagerTest { underTest.removeListener(listener1) awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyNoMoreInteractions(windowManager) } @Test @@ -119,12 +121,12 @@ class GestureExclusionManagerTest { awaitTasksCompleted() underTest.addListener(listener1) awaitTasksCompleted() - verifyZeroInteractions(listener1) + verifyNoMoreInteractions(listener1) underTest.addListener(listener2) awaitTasksCompleted() - verifyZeroInteractions(listener1) + verifyNoMoreInteractions(listener1) verify(listener2).onGestureExclusionChanged(r1, r2) } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt new file mode 100644 index 000000000..9f491717c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt @@ -0,0 +1,109 @@ +/* + * 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.quickstep.util + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Rect +import android.view.Display.DEFAULT_DISPLAY +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +class GroupTaskTest { + + @Test + fun testGroupTask_sameInstance_isEqual() { + val task = SingleTask(createTask(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testGroupTask_identicalConstructor_isEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = SingleTask(createTask(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_copy_isEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentId_isNotEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = SingleTask(createTask(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_equalSplitTasks_isEqual() { + val splitBounds = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_50_50, + ) + val task1 = SplitTask(createTask(1), createTask(2), splitBounds) + val task2 = SplitTask(createTask(1), createTask(2), splitBounds) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentSplitTasks_isNotEqual() { + val splitBounds1 = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_50_50, + ) + val splitBounds2 = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_33_66, + ) + val task1 = SplitTask(createTask(1), createTask(2), splitBounds1) + val task2 = SplitTask(createTask(1), createTask(2), splitBounds2) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_differentType_isNotEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, listOf(createTask(1))) + assertThat(task1).isNotEqualTo(task2) + } + + private fun createTask(id: Int): Task { + return Task(Task.TaskKey(id, 0, Intent(), ComponentName("", ""), 0, 0)) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt new file mode 100644 index 000000000..3cdf60805 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateHandlerTests.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.view.Surface +import android.view.Surface.ROTATION_0 +import android.view.Surface.ROTATION_180 +import android.view.Surface.ROTATION_90 +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.FallbackActivityInterface +import com.android.quickstep.orientation.RecentsPagedOrientationHandler +import com.android.quickstep.orientation.RecentsPagedOrientationHandler.Companion.PORTRAIT +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +/** + * Test all possible inputs to RecentsOrientedState.updateHandler. It tests all possible + * combinations of rotations and relevant methods (two methods that return boolean values) but it + * only provides the expected result when the final rotation is different from ROTATION_0 for + * simplicity. So any case not shown in resultMap you can assume results in ROTATION_0. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class RecentOrientedStateHandlerTests { + + data class TestCase( + val recentsRotation: Int, + val displayRotation: Int, + val touchRotation: Int, + val isRotationAllowed: Boolean, + val isFixedLandscape: Boolean, + ) { + override fun toString(): String { + return "TestCase(recentsRotation=${Surface.rotationToString(recentsRotation)}, " + + "displayRotation=${Surface.rotationToString(displayRotation)}, " + + "touchRotation=${Surface.rotationToString(touchRotation)}, " + + "isRotationAllowed=$isRotationAllowed, " + + "isFixedLandscape=$isFixedLandscape)" + } + } + + private fun runTestCase(testCase: TestCase, expectedHandler: RecentsPagedOrientationHandler) { + val recentOrientedState = + spy( + RecentsOrientedState( + ApplicationProvider.getApplicationContext(), + FallbackActivityInterface.INSTANCE, + ) {} + ) + whenever(recentOrientedState.isRecentsActivityRotationAllowed).thenAnswer { + testCase.isRotationAllowed + } + whenever(recentOrientedState.isLauncherFixedLandscape).thenAnswer { + testCase.isFixedLandscape + } + + recentOrientedState.update(testCase.displayRotation, testCase.touchRotation) + val rotation = recentOrientedState.orientationHandler.rotation + assertWithMessage("$testCase to ${Surface.rotationToString(rotation)},") + .that(rotation) + .isEqualTo(expectedHandler.rotation) + } + + @Test + fun `test fixed landscape when device is portrait`() { + runTestCase( + TestCase( + recentsRotation = ROTATION_0, + displayRotation = -1, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } + + @Test + fun `test fixed landscape when device is landscape`() { + runTestCase( + TestCase( + recentsRotation = ROTATION_90, + displayRotation = -1, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } + + @Test + fun `test fixed landscape when device is seascape`() { + runTestCase( + TestCase( + recentsRotation = ROTATION_180, + displayRotation = -1, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } + + @Test + fun `test fixed landscape when device is portrait and display rotation is portrait`() { + runTestCase( + TestCase( + recentsRotation = ROTATION_0, + displayRotation = ROTATION_0, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } + + @Test + fun `test fixed landscape when device is landscape and display rotation is landscape `() { + runTestCase( + TestCase( + recentsRotation = ROTATION_90, + displayRotation = ROTATION_90, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } + + @Test + fun `test fixed landscape when device is seascape and display rotation is seascape`() { + runTestCase( + TestCase( + recentsRotation = ROTATION_180, + displayRotation = ROTATION_180, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + PORTRAIT, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index d40f8ab38..c9d7e1dee 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -27,15 +27,15 @@ import android.view.View import android.window.TransitionInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.apppairs.AppPairIcon +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.views.GroupedTaskView import com.android.quickstep.views.IconView -import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskView -import com.android.quickstep.views.TaskView.TaskContainer import com.android.systemui.shared.recents.model.Task import org.junit.Assert.assertEquals import org.junit.Before @@ -59,7 +59,7 @@ class SplitAnimationControllerTest { private val mockSplitSelectStateController: SplitSelectStateController = mock() // TaskView private val mockTaskView: TaskView = mock() - private val mockThumbnailView: TaskThumbnailViewDeprecated = mock() + private val mockSnapshotView: View = mock() private val mockBitmap: Bitmap = mock() private val mockIconView: IconView = mock() private val mockTaskViewDrawable: Drawable = mock() @@ -77,6 +77,7 @@ class SplitAnimationControllerTest { private val splitSelectSource: SplitConfigurationOptions.SplitSelectSource = mock() private val mockSplitSourceDrawable: Drawable = mock() private val mockSplitSourceView: View = mock() + private val mockItemInfo: ItemInfo = mock() private val stateManager: StateManager<*, *> = mock() private val depthController: DepthController = mock() @@ -87,14 +88,17 @@ class SplitAnimationControllerTest { @Before fun setup() { - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) - whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView) + whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) + whenever(mockTaskContainer.task).thenReturn(mockTask) whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable) whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer }) + whenever(mockTaskView.firstTaskContainer).thenReturn(mockTaskContainer) whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable) whenever(splitSelectSource.view).thenReturn(mockSplitSourceView) + whenever(splitSelectSource.itemInfo).thenReturn(mockItemInfo) splitAnimationController = SplitAnimationController(mockSplitSelectStateController) } @@ -114,7 +118,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not fallback to use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -130,7 +134,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use taskView icon drawable", mockTaskViewDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -149,7 +153,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use taskView icon drawable", mockTaskViewDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -165,7 +169,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -180,7 +184,6 @@ class SplitAnimationControllerTest { whenever(mockTaskContainer.task).thenReturn(mockTask) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) whenever(mockTask.getKey()).thenReturn(mockTaskKey) whenever(mockTaskKey.getId()).thenReturn(taskId) whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId) @@ -188,13 +191,13 @@ class SplitAnimationControllerTest { val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps = splitAnimationController.getFirstAnimInitViews( { mockGroupedTaskView }, - { splitSelectSource } + { splitSelectSource }, ) assertEquals( "Did not use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -212,7 +215,7 @@ class SplitAnimationControllerTest { any(), any(), any(), - any() + any(), ) spySplitAnimationController.playSplitLaunchAnimation( @@ -227,7 +230,8 @@ class SplitAnimationControllerTest { depthController, null /* info */, null /* t */, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -240,7 +244,7 @@ class SplitAnimationControllerTest { any(), any(), any(), - any() + any(), ) } @@ -263,7 +267,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -276,7 +281,7 @@ class SplitAnimationControllerTest { whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper) doNothing() .whenever(spySplitAnimationController) - .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any()) doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( @@ -291,11 +296,12 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) - .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any()) } @Test @@ -319,7 +325,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -346,7 +353,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -373,7 +381,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -385,7 +394,7 @@ class SplitAnimationControllerTest { val spySplitAnimationController = spy(splitAnimationController) doNothing() .whenever(spySplitAnimationController) - .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) + .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, @@ -399,10 +408,11 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) - .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) + .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any()) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index bab84ef28..e4bdba510 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -22,7 +22,6 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Intent import android.graphics.Rect -import android.os.Handler import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.LauncherState @@ -37,9 +36,10 @@ import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController +import com.android.quickstep.views.RecentsView import com.android.quickstep.views.RecentsViewContainer import com.android.systemui.shared.recents.model.Task -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -52,6 +52,7 @@ import org.mockito.Mockito.any import org.mockito.Mockito.`when` import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -63,11 +64,11 @@ class SplitSelectStateControllerTest { private val statsLogManager: StatsLogManager = mock() private val statsLogger: StatsLogger = mock() private val stateManager: StateManager> = mock() - private val handler: Handler = mock() private val context: RecentsViewContainer = mock() private val recentsModel: RecentsModel = mock() private val pendingIntent: PendingIntent = mock() private val splitFromDesktopController: SplitFromDesktopController = mock() + private val recentsView: RecentsView<*, *> = mock() private lateinit var splitSelectStateController: SplitSelectStateController @@ -75,6 +76,7 @@ class SplitSelectStateControllerTest { private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10) private var taskIdCounter = 0 + private fun getUniqueId(): Int { return ++taskIdCounter } @@ -87,13 +89,12 @@ class SplitSelectStateControllerTest { splitSelectStateController = SplitSelectStateController( context, - handler, stateManager, depthController, statsLogManager, systemUiProxy, recentsModel, - null /*activityBackCallback*/ + null, /*activityBackCallback*/ ) } @@ -101,14 +102,14 @@ class SplitSelectStateControllerTest { fun activeTasks_noMatchingTasks() { val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("hotdog", "juice"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -125,7 +126,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -142,14 +143,14 @@ class SplitSelectStateControllerTest { val matchingComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pomegranate", "juice") + ComponentName("pomegranate", "juice"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -162,14 +163,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -178,7 +179,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -195,14 +196,14 @@ class SplitSelectStateControllerTest { val nonPrimaryUserComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pomegranate", "juice") + ComponentName("pomegranate", "juice"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -219,7 +220,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -236,16 +237,16 @@ class SplitSelectStateControllerTest { val nonPrimaryUserComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle, ComponentName("pomegranate", "juice"), - nonPrimaryUserHandle + nonPrimaryUserHandle, ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -258,15 +259,15 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -275,7 +276,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -292,14 +293,14 @@ class SplitSelectStateControllerTest { val matchingComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -312,14 +313,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -328,7 +329,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -347,11 +348,11 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -366,14 +367,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[1].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[1].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[1], groupTask2.task2) + assertEquals(it[1], groupTask2.bottomRightTask) } // Capture callback from recentsModel#getTasks() @@ -382,7 +383,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -400,11 +401,11 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -418,14 +419,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask2.task2) + assertEquals(it[0], groupTask2.bottomRightTask) assertNull("No tasks should have matched", it[1] /*task*/) } @@ -435,7 +436,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -453,14 +454,14 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -474,25 +475,25 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) assertEquals( "ComponentName package mismatched", it[1].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[1].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[1], groupTask2.task2) + assertEquals(it[1], groupTask2.bottomRightTask) } // Capture callback from recentsModel#getTasks() @@ -501,7 +502,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -523,16 +524,16 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage2, matchingClass2), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage2, matchingClass2), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val groupTask3 = - generateGroupTask( + generateSplitTask( ComponentName("hotdog", "pie"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask3) @@ -544,7 +545,7 @@ class SplitSelectStateControllerTest { val taskConsumer = Consumer> { assertEquals("Expected array length 2", 2, it.size) - assertEquals("Found wrong task", it[0], groupTask2.task1) + assertEquals("Found wrong task", it[0], groupTask2.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -553,7 +554,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent2, matchingComponent), true /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -570,7 +571,7 @@ class SplitSelectStateControllerTest { -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - 10 /*alreadyRunningTask*/ + 10, /*alreadyRunningTask*/ ) assertTrue(splitSelectStateController.isSplitSelectActive) } @@ -582,21 +583,23 @@ class SplitSelectStateControllerTest { -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - -1 /*alreadyRunningTask*/ + -1, /*alreadyRunningTask*/ ) assertTrue(splitSelectStateController.isSplitSelectActive) } @Test fun resetAfterInitial() { + whenever(context.getOverviewPanel>()).thenReturn(recentsView) splitSelectStateController.setInitialTaskSelect( Intent() /*intent*/, -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - -1 + -1, ) splitSelectStateController.resetState() + verify(recentsView, times(1)).resetDesktopTaskFromSplitSelectState() assertFalse(splitSelectStateController.isSplitSelectActive) } @@ -622,11 +625,26 @@ class SplitSelectStateControllerTest { verify(splitFromDesktopController).onDestroy() } - // Generate GroupTask with default userId. - private fun generateGroupTask( + @Test + fun splitSelectStateControllerDestroyed_doNotResetDeskTopTasks() { + whenever(context.getOverviewPanel>()).thenReturn(recentsView) + splitSelectStateController.setInitialTaskSelect( + Intent(), /*intent*/ + -1, /*stagePosition*/ + ItemInfo(), + null, /*splitEvent*/ + -1, + ) + splitSelectStateController.onDestroy() + splitSelectStateController.resetState() + verify(recentsView, times(0)).resetDesktopTaskFromSplitSelectState() + } + + /** Generates a [SplitTask] with default userId. */ + private fun generateSplitTask( task1ComponentName: ComponentName, - task2ComponentName: ComponentName - ): GroupTask { + task2ComponentName: ComponentName, + ): SplitTask { val task1 = Task() var taskInfo = ActivityManager.RunningTaskInfo() taskInfo.taskId = getUniqueId() @@ -642,20 +660,26 @@ class SplitSelectStateControllerTest { intent.component = task2ComponentName taskInfo.baseIntent = intent task2.key = Task.TaskKey(taskInfo) - return GroupTask( + return SplitTask( task1, task2, - SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50) + SplitConfigurationOptions.SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ task1.key.id, + /* rightBottomTaskId = */ task2.key.id, + /* snapPosition = */ SNAP_TO_2_50_50, + ), ) } - // Generate GroupTask with custom user handles. - private fun generateGroupTask( + /** Generates a [SplitTask] with custom user handles. */ + private fun generateSplitTask( task1ComponentName: ComponentName, userHandle1: UserHandle, task2ComponentName: ComponentName, - userHandle2: UserHandle - ): GroupTask { + userHandle2: UserHandle, + ): SplitTask { val task1 = Task() var taskInfo = ActivityManager.RunningTaskInfo() taskInfo.taskId = getUniqueId() @@ -674,10 +698,16 @@ class SplitSelectStateControllerTest { intent.component = task2ComponentName taskInfo.baseIntent = intent task2.key = Task.TaskKey(taskInfo) - return GroupTask( + return SplitTask( task1, task2, - SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50) + SplitConfigurationOptions.SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ task1.key.id, + /* rightBottomTaskId = */ task2.key.id, + /* snapPosition = */ SNAP_TO_2_50_50, + ), ) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java deleted file mode 100644 index 7ef4910ce..000000000 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java +++ /dev/null @@ -1,510 +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.quickstep.util; - -import static com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID; -import static com.android.quickstep.util.TaskGridNavHelper.INVALID_FOCUSED_TASK_ID; - -import static org.junit.Assert.assertEquals; - -import com.android.launcher3.util.IntArray; - -import org.junit.Test; - -public class TaskGridNavHelperTest { - - @Test - public void equalLengthRows_noFocused_onTop_pressDown_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressUp_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressDown_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressUp_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressLeft_goesLeft() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 3, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 4, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 3; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 4; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 5; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 6; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 6, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressLeft_toTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 7; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 7; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 6; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atClearAll_pressRight_goToLonger() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6, 7); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressTab_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 3, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 3; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt new file mode 100644 index 000000000..cb088fda5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt @@ -0,0 +1,858 @@ +/* + * 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.quickstep.util + +import com.android.launcher3.util.IntArray +import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID +import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TaskGridNavHelperTest { + + /* + 5 3 1 + CLEAR_ALL ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2) + } + + /* ↑----→ + 5 3 1 | + CLEAR_ALL | + 6 4 2←---| + */ + @Test + fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2) + } + + /* ↓----↑ + 5 3 1 | + CLEAR_ALL | + 6 4 2 | + ↓----→ + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1) + } + + /* + 5 3<--1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL + 6 4<--2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4) + } + + /* + 5 3-->1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() { + assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL + 6 4-->2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() { + assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2) + } + + /* + ↓------------------← + | | + ↓ 5 3 1---→ + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + ↓------------------← + | ↑ + ↓ 5 3 1 | + CLEAR_ALL ↑ + 6 4 2---→ + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + ←----5 3 1 + ↓ + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL + ↑ + ←---6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→-----------------------| + | ↓ + ↑ 5 3 1 + ←------CLEAR_ALL + + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() { + assertThat( + getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1) + ) + .isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL--↓ + | + |--→6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() { + assertThat( + getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1) + ) + .isEqualTo(6) + } + + /* + 5 3 1←--- + ↑ + CLEAR_ALL ←--FOCUSED_TASK + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressLeft_toTop() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + LEFT, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(1) + } + + /* + 5 3 1 + ←--↑ + CLEAR_ALL ↓-→FOCUSED_TASK + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + UP, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL ↑--→FOCUSED_TASK + ↑←--↓ + 6 4 2 + */ + + @Test + fun equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + DOWN, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + ↓-------------------------------←| + | ↑ + ↓ 5 3 1 | + CLEAR_ALL FOCUSED_TASK--→ + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→---------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL FOCUSED_TASK + + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 7←-↑ 5 3 1 + ↓--→ + CLEAR_ALL + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 7, + DOWN, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + ←--↑ + ↓-→7 5 3 1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = 7, + UP, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 7 5 3 1 + CLEAR_ALL ↑ + ←----6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = 6, + LEFT, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 7 5 3 1 + ↑ + CLEAR_ALL-----→ + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atClearAll_pressRight_goToLonger() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + RIGHT, + delta = -1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 5 3 1 + CLEAR_ALL-----→ + ↓ + 7 6 4 2 + */ + @Test + fun longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + RIGHT, + delta = -1, + bottomIds = IntArray.wrap(2, 4, 6, 7), + ) + ) + .isEqualTo(7) + } + + /* + 5 3 1 + CLEAR_ALL ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + ←---↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL ↓ + ----→ + ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1) + } + + /* + 5 3 [1] + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false)) + .isEqualTo(1) + } + + /* + 5 3 1 + [CLEAR_ALL] + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressTab_noCycle_staysOnClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + TAB, + delta = 1, + cycle = false, + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK←--DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressLeftFromDesktopTask_goesToFocusedTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK--→DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromFocusedTask_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + ↓-----------------------------------------←| + | | + ↓ 5 3 1 ↑ + CLEAR_ALL FOCUSED_TASK DESKTOP--→ + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromDesktopTask_goesToClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→-------------------------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL FOCUSED_TASK DESKTOP + + 6 4 2 + */ + @Test + fun withLargeTile_pressLeftFromClearAll_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK DESKTOP + ↑ + 6 4 2→----↑ + */ + @Test + fun withLargeTile_pressRightFromBottom_goesToLargeTile() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 2, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1→----| + ↓ + CLEAR_ALL FOCUSED_TASK DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromTop_goesToLargeTile() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 1, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + + CLEAR_ALL FOCUSED_TASK←---DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressTabFromDeskTop_goesToFocusedTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + TAB, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + CLEAR_ALL FOCUSED_TASK DESKTOP + ↓ + 2←----↓ + */ + @Test + fun withLargeTile_pressLeftFromLargeTile_goesToBottom() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + LEFT, + delta = 1, + topIds = IntArray(), + bottomIds = IntArray.wrap(2), + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(2) + } + + /* + ↓-----------------------------------------←| + | | + ↓ 5 3 1 ↑ + CLEAR_ALL FOCUSED_TASK DESKTOP--→ + 6 4 2 + */ + @Test + fun withLargeTile_pressShiftTabFromDeskTop_goesToClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + TAB, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1→----| + ↓ + CLEAR_ALL ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromTop_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 1, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL ADD_DESKTOP + ↑ + 6 4 2→----↑ + */ + @Test + fun withAddDesktopButton_pressRightFromBottom_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 2, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + ↓-------------------------------←| + | ↑ + ↓ 5 3 1 | + CLEAR_ALL ADD_DESKTOP--→ + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromAddDesktopButton_goesToClearAllButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→--------------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL ADD_DESKTOP + + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressLeftFromClearAllButton_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + ←--↑ + CLEAR_ALL ↓-→ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressUpOnAddDesktop_stayOnAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + UP, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL ↑--→ADD_DESKTOP + ↑←--↓ + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressDownOnAddDesktop_stayOnAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + DOWN, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL DESKTOP--→ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromDesktopTask_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID), + hasAddDesktopButton = true, + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL DESKTOP←--ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressLeftFromAddDesktopButton_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID), + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + // Col offset: 0 1 2 + // ----------- + // ID grid: 4 2 0 start + // end [5] 3 1 + @Test + fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() { + val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2)) + assertThat( + gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId = 5, + towardsStart = true, + ) + .toList() + ) + .isEqualTo(expected) + } + + // Col offset: 2 1 0 + // ----------- + // ID grid: 4 2 [0] start + // end 5 3 1 + @Test + fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() { + val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2)) + assertThat( + gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId = 0, + towardsStart = false, + ) + .toList() + ) + .isEqualTo(expected) + } + + private fun getNextGridPage( + currentPageTaskViewId: Int, + direction: TaskGridNavHelper.TaskNavDirection, + delta: Int, + topIds: IntArray = IntArray.wrap(1, 3, 5), + bottomIds: IntArray = IntArray.wrap(2, 4, 6), + largeTileIds: List = emptyList(), + hasAddDesktopButton: Boolean = false, + cycle: Boolean = true, + ): Int { + val taskGridNavHelper = + TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton) + return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle) + } + + private fun gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId: Int, + towardsStart: Boolean, + topIds: IntArray = IntArray.wrap(0, 2, 4), + bottomIds: IntArray = IntArray.wrap(1, 3, 5), + largeTileIds: List = emptyList(), + hasAddDesktopButton: Boolean = false, + ): Sequence> { + val taskGridNavHelper = + TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton) + return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId, + towardsStart, + ) + } + + private companion object { + const val FOCUSED_TASK_ID = 99 + const val DESKTOP_TASK_ID = 100 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java index 72cfd92b8..be76f9e86 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java @@ -35,6 +35,9 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.util.AllModulesMinusWMProxy; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.LauncherModelHelper; @@ -46,6 +49,9 @@ import com.android.launcher3.util.window.WindowManagerProxy; import com.android.quickstep.FallbackActivityInterface; import com.android.quickstep.util.SurfaceTransaction.MockProperties; +import dagger.BindsInstance; +import dagger.Component; + import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.Assert; @@ -159,6 +165,11 @@ public class TaskViewSimulatorTest { void verifyNoTransforms() { LauncherModelHelper helper = new LauncherModelHelper(); try { + DisplayController mockController = mock(DisplayController.class); + + helper.sandboxContext.initDaggerComponent( + DaggerTaskViewSimulatorTest_TaskViewSimulatorTestComponent.builder() + .bindDisplayController(mockController)); int rotation = mDisplaySize.x > mDisplaySize.y ? Surface.ROTATION_90 : Surface.ROTATION_0; CachedDisplayInfo cdi = new CachedDisplayInfo(mDisplaySize, rotation); @@ -192,17 +203,14 @@ public class TaskViewSimulatorTest { DisplayController.Info info = new Info( configurationContext, wmProxy, perDisplayBoundsCache); - - DisplayController mockController = mock(DisplayController.class); when(mockController.getInfo()).thenReturn(info); - helper.sandboxContext.putObject(DisplayController.INSTANCE, mockController); mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(helper.sandboxContext) .getBestMatch(mAppBounds.width(), mAppBounds.height(), rotation); mDeviceProfile.updateInsets(mLauncherInsets); TaskViewSimulator tvs = new TaskViewSimulator(helper.sandboxContext, - FallbackActivityInterface.INSTANCE); + FallbackActivityInterface.INSTANCE, false, 0); tvs.setDp(mDeviceProfile); int launcherRotation = info.rotation; @@ -271,4 +279,18 @@ public class TaskViewSimulatorTest { description.appendValue(mExpected); } } + + @LauncherAppSingleton + @Component(modules = {AllModulesMinusWMProxy.class}) + interface TaskViewSimulatorTestComponent extends LauncherAppComponent { + + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + + @BindsInstance + Builder bindDisplayController(DisplayController controller); + + TaskViewSimulatorTestComponent build(); + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt new file mode 100644 index 000000000..6c526a485 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt @@ -0,0 +1,65 @@ +/* + * 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.quickstep.util + +import com.android.launcher3.BuildConfig +import com.android.launcher3.util.SafeCloseable +import com.android.quickstep.DeviceConfigWrapper.Companion.configHelper +import com.android.quickstep.util.DeviceConfigHelper.Companion.prefs +import java.util.concurrent.CountDownLatch +import java.util.function.BooleanSupplier +import org.junit.Assert +import org.junit.Assume + +/** Helper methods for testing */ +object TestExtensions { + + @JvmStatic + fun overrideNavConfigFlag( + key: String, + value: Boolean, + targetValue: BooleanSupplier + ): AutoCloseable { + Assume.assumeTrue(BuildConfig.IS_DEBUG_DEVICE) + if (targetValue.asBoolean == value) { + return AutoCloseable {} + } + + navConfigEditWatcher().let { + prefs.edit().putBoolean(key, value).commit() + it.close() + } + Assert.assertEquals(value, targetValue.asBoolean) + + val watcher = navConfigEditWatcher() + return AutoCloseable { + prefs.edit().remove(key).commit() + watcher.close() + } + } + + private fun navConfigEditWatcher(): SafeCloseable { + val wait = CountDownLatch(1) + val listener = Runnable { wait.countDown() } + configHelper.addChangeListener(listener) + + return SafeCloseable { + wait.await() + configHelper.removeChangeListener(listener) + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt new file mode 100644 index 000000000..6dbb667a2 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.TransitionInfo.FLAG_NONE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.RemoteAnimationTargets +import com.android.quickstep.util.TransformParams.BuilderProxy.NO_OP +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TransformParamsTest { + private val surfaceTransaction = mock() + private val transaction = mock() + private val transformParams = TransformParams(::surfaceTransaction) + + private val freeformTaskInfo1 = + createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FREEFORM) + private val freeformTaskInfo2 = + createTaskInfo(taskId = 2, windowingMode = WINDOWING_MODE_FREEFORM) + private val fullscreenTaskInfo1 = + createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FULLSCREEN) + + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + + @Before + fun setUp() { + whenever(surfaceTransaction.transaction).thenReturn(transaction) + whenever(surfaceTransaction.forSurface(anyOrNull())) + .thenReturn(mock()) + transformParams.setCornerRadius(CORNER_RADIUS) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_freeformTasks_overridesCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction).setCornerRadius(leash1, 0f) + verify(transaction).setCornerRadius(leash2, 0f) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_freeformTasks_overridesCornerRadiusOnlyOnce() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + transformParams.createSurfaceParams(NO_OP) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction).setCornerRadius(leash1, 0f) + verify(transaction).setCornerRadius(leash2, 0f) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_flagDisabled_doesntOverrideCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction, never()).setCornerRadius(leash1, 0f) + verify(transaction, never()).setCornerRadius(leash2, 0f) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_fullscreenTasks_doesntOverrideCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash = mock() + transitionInfo.addChange(createChange(fullscreenTaskInfo1, leash = leash)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(fullscreenTaskInfo1))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction, never()).setCornerRadius(leash, 0f) + } + + private fun createTargetSet(taskInfos: List): RemoteAnimationTargets { + val remoteAnimationTargets = mutableListOf() + taskInfos.map { remoteAnimationTargets.add(createRemoteAnimationTarget(it)) } + return RemoteAnimationTargets( + remoteAnimationTargets.toTypedArray(), + /* wallpapers= */ null, + /* nonApps= */ null, + /* targetMode= */ TRANSIT_OPEN, + ) + } + + private fun createRemoteAnimationTarget(taskInfo: RunningTaskInfo): RemoteAnimationTarget { + val windowConfig = mock() + whenever(windowConfig.activityType).thenReturn(ACTIVITY_TYPE_STANDARD) + return RemoteAnimationTarget( + taskInfo.taskId, + /* mode= */ TRANSIT_OPEN, + /* leash= */ null, + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + windowConfig, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + taskInfo, + /* allowEnterPip= */ false, + ) + } + + private fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo { + val taskInfo = RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowingMode + return taskInfo + } + + private fun createChange(taskInfo: RunningTaskInfo, leash: SurfaceControl): Change { + val taskInfo = createTaskInfo(taskInfo.taskId, taskInfo.windowingMode) + val change = Change(taskInfo.token, mock()) + change.mode = TRANSIT_OPEN + change.taskInfo = taskInfo + change.leash = leash + return change + } + + private companion object { + private const val CORNER_RADIUS = 30f + } +} diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index 7b57c81b7..59ce6370e 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -20,6 +20,7 @@ import static android.content.pm.ApplicationInfo.FLAG_INSTALLED; import static android.os.Process.myUserHandle; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; @@ -41,11 +42,12 @@ import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; +import android.os.Process; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; -import androidx.test.core.content.pm.ApplicationInfoBuilder; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -62,6 +64,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.List; @@ -71,6 +75,9 @@ import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public final class WidgetsPredicationUpdateTaskTest { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -80,6 +87,7 @@ public final class WidgetsPredicationUpdateTaskTest { private AppWidgetProviderInfo mApp4Provider1; private AppWidgetProviderInfo mApp4Provider2; private AppWidgetProviderInfo mApp5Provider1; + private AppWidgetProviderInfo mApp6PinOnlyProvider1; private List allWidgets; private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); @@ -106,16 +114,22 @@ public final class WidgetsPredicationUpdateTaskTest { ComponentName.createRelative("app4", ".provider2")); mApp5Provider1 = createAppWidgetProviderInfo( ComponentName.createRelative("app5", "provider1")); + mApp6PinOnlyProvider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app6", "provider1"), + /*hideFromPicker=*/ true + ); + + allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, - mApp4Provider1, mApp4Provider2, mApp5Provider1); + mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1); mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class); doAnswer(i -> { String pkg = i.getArgument(0); - ApplicationInfo applicationInfo = ApplicationInfoBuilder.newBuilder() - .setPackageName(pkg) - .setName("App " + pkg) - .build(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = pkg; + applicationInfo.name = "App " + pkg; + applicationInfo.uid = Process.myUid(); applicationInfo.category = CATEGORY_PRODUCTIVITY; applicationInfo.flags = FLAG_INSTALLED; return applicationInfo; @@ -145,6 +159,7 @@ public final class WidgetsPredicationUpdateTaskTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() { // Run on model executor so that no other task runs in the middle. runOnExecutorSync(MODEL_EXECUTOR, () -> { @@ -184,6 +199,7 @@ public final class WidgetsPredicationUpdateTaskTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off public void widgetsRecommendationRan_shouldReturnEmptyWidgetsWhenEmpty() { runOnExecutorSync(MODEL_EXECUTOR, () -> { @@ -213,6 +229,32 @@ public final class WidgetsPredicationUpdateTaskTest { }); } + @Test + public void widgetsRecommendations_excludesWidgetsHiddenForPicker() { + runOnExecutorSync(MODEL_EXECUTOR, () -> { + + // Not installed widget - hence eligible + AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", + mUserHandle); + // Provider marked as hidden from picker - hence not eligible + AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1", + mUserHandle); + + mCallback.mRecommendedWidgets = null; + mModelHelper.getModel().enqueueModelUpdateTask( + newWidgetsPredicationTask(List.of(widget1, widget6))); + runOnExecutorSync(MAIN_EXECUTOR, () -> { }); + + // Only widget 1 (and no widget 6 as its meant to be hidden from picker). + List recommendedWidgets = mCallback.mRecommendedWidgets.items + .stream() + .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) + .collect(Collectors.toList()); + assertThat(recommendedWidgets).hasSize(1); + assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1"); + }); + } + private void assertWidgetInfo( LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) { assertThat(actual.provider).isEqualTo(expected.provider); @@ -221,7 +263,8 @@ public final class WidgetsPredicationUpdateTaskTest { private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List appTargets) { return new WidgetsPredictionUpdateTask( - new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), + new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction", + DEFAULT_LOOKUP_FLAG), appTargets); } diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt new file mode 100644 index 000000000..17cca0b7d --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.statehandlers + +import android.content.res.Resources +import android.view.ViewTreeObserver +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Launcher +import com.android.launcher3.R +import com.android.launcher3.dragndrop.DragLayer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.same +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DepthControllerTest { + + private lateinit var underTest: DepthController + @Mock private lateinit var launcher: Launcher + @Mock private lateinit var resource: Resources + @Mock private lateinit var dragLayer: DragLayer + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(launcher.resources).thenReturn(resource) + `when`(resource.getInteger(R.integer.max_depth_blur_radius)).thenReturn(30) + `when`(launcher.dragLayer).thenReturn(dragLayer) + `when`(dragLayer.viewTreeObserver).thenReturn(viewTreeObserver) + + underTest = DepthController(launcher) + } + + @Test + fun setActivityStarted_add_onDrawListener() { + underTest.setActivityStarted(true) + + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStopped_not_remove_onDrawListener() { + underTest.setActivityStarted(false) + + // Because underTest.mOnDrawListener is never added + verifyNoMoreInteractions(viewTreeObserver) + } + + @Test + fun setActivityStared_then_stopped_remove_onDrawListener() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStared_then_stopped_multiple_times_remove_onDrawListener_once() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + + // Should just remove mOnDrawListener once + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun test_onInvalidSurface_multiple_times_add_onDrawListener_once() { + underTest.onInvalidSurface() + underTest.onInvalidSurface() + underTest.onInvalidSurface() + + // We should only call addOnDrawListener 1 time + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt new file mode 100644 index 000000000..4b8f2a234 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.statehandlers + +import android.content.Context +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.quickstep.SystemUiProxy +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** + * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple + * displays. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopVisibilityControllerTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus::class.java) + .startMocking() + + private val context = mock() + private val systemUiProxy = mock() + private val lifeCycleTracker = mock() + private lateinit var desktopVisibilityController: DesktopVisibilityController + + @Before + fun setUp() { + whenever(context.resources).thenReturn(mock()) + whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true) + desktopVisibilityController = + DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND) + fun noCrashWhenCheckingNonExistentDisplay() { + assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500)) + assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300)) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java deleted file mode 100644 index 9ed39066d..000000000 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java +++ /dev/null @@ -1,219 +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.launcher3.taskbar; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.Display; -import android.view.MotionEvent; - -import androidx.test.filters.SmallTest; - -import com.android.launcher3.BubbleTextView; -import com.android.launcher3.folder.Folder; -import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.model.data.FolderInfo; -import com.android.launcher3.util.ActivityContextWrapper; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -/** - * Tests for TaskbarHoverToolTipController. - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { - - private TaskbarHoverToolTipController mTaskbarHoverToolTipController; - private TestableLooper mTestableLooper; - - @Mock private TaskbarView mTaskbarView; - @Mock private MotionEvent mMotionEvent; - @Mock private BubbleTextView mHoverBubbleTextView; - @Mock private FolderIcon mHoverFolderIcon; - @Mock private Display mDisplay; - @Mock private TaskbarDragLayer mTaskbarDragLayer; - private Folder mSpyFolderView; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - Context context = getApplicationContext(); - - doAnswer((Answer) invocation -> context.getSystemService( - (String) invocation.getArgument(0))) - .when(taskbarActivityContext).getSystemService(anyString()); - when(taskbarActivityContext.getResources()).thenReturn(context.getResources()); - when(taskbarActivityContext.getApplicationInfo()).thenReturn( - context.getApplicationInfo()); - when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer); - when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper()); - when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay); - - when(mTaskbarDragLayer.getChildCount()).thenReturn(1); - mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null)); - when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView); - doReturn(false).when(mSpyFolderView).isOpen(); - - when(mHoverBubbleTextView.getText()).thenReturn("tooltip"); - doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - ((int[]) args[0])[0] = 0; - ((int[]) args[0])[1] = 0; - return null; - }).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class)); - when(mHoverBubbleTextView.getWidth()).thenReturn(100); - when(mHoverBubbleTextView.getHeight()).thenReturn(100); - - mHoverFolderIcon.mInfo = new FolderInfo(); - mHoverFolderIcon.mInfo.title = "tooltip"; - doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - ((int[]) args[0])[0] = 0; - ((int[]) args[0])[1] = 0; - return null; - }).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class)); - when(mHoverFolderIcon.getWidth()).thenReturn(100); - when(mHoverFolderIcon.getHeight()).thenReturn(100); - - when(mTaskbarView.getTop()).thenReturn(200); - - mTaskbarHoverToolTipController = new TaskbarHoverToolTipController( - taskbarActivityContext, mTaskbarView, mHoverBubbleTextView); - mTestableLooper = TestableLooper.get(this); - } - - @Test - public void onHover_hoverEnterIcon_revealToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - true); - } - - @Test - public void onHover_hoverExitIcon_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverEnterFolderIcon_revealToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - true); - } - - @Test - public void onHover_hoverExitFolderIcon_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverExitFolderOpen_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - doReturn(true).when(mSpyFolderView).isOpen(); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverEnterFolderOpen_noToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - doReturn(true).when(mSpyFolderView).isOpen(); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - - assertThat(hoverHandled).isFalse(); - } - - @Test - public void onHover_hoverMove_noUpdate() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - - assertThat(hoverHandled).isFalse(); - } - - private void waitForIdleSync() { - mTestableLooper.processAllMessages(); - } -} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt deleted file mode 100644 index 104263af5..000000000 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ /dev/null @@ -1,242 +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.launcher3.taskbar - -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.content.ComponentName -import android.content.Intent -import android.os.Process -import android.os.UserHandle -import android.testing.AndroidTestingRunner -import com.android.launcher3.model.data.AppInfo -import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.statehandlers.DesktopVisibilityController -import com.android.quickstep.RecentsModel -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.kotlin.whenever - -@RunWith(AndroidTestingRunner::class) -class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { - - @get:Rule val mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var mockRecentsModel: RecentsModel - @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController - - private var nextTaskId: Int = 500 - - private lateinit var recentAppsController: TaskbarRecentAppsController - private lateinit var userHandle: UserHandle - - @Before - fun setUp() { - super.setup() - userHandle = Process.myUserHandle() - recentAppsController = - TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController } - recentAppsController.init(taskbarControllers) - recentAppsController.isEnabled = true - recentAppsController.setApps( - ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray() - ) - } - - @Test - fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) - val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(hotseatPackages) - } - - @Test - fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - ) - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(expectedPackages) - } - - @Test - fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames( - listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - ) - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(expectedPackages) - } - - @Test - fun getRunningApps_notInDesktopMode_returnsEmptySet() { - setInDesktopMode(false) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps).isEmpty() - assertThat(recentAppsController.minimizedApps).isEmpty() - } - - @Test - fun getRunningApps_inDesktopMode_returnsRunningApps() { - setInDesktopMode(true) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - assertThat(recentAppsController.minimizedApps).isEmpty() - } - - @Test - fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() { - setInDesktopMode(true) - val runningTasks = - ArrayList( - listOf( - createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false }, - ) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) - assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3) - } - - private fun createHotseatItemsFromPackageNames(packageNames: List): List { - return packageNames.map { createTestAppInfo(packageName = it) } - } - - private fun createDesktopTasksFromPackageNames( - packageNames: List - ): ArrayList { - return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) }) - } - - private fun createDesktopTaskInfo( - packageName: String, - init: RunningTaskInfo.() -> Unit = { isVisible = true }, - ): RunningTaskInfo { - return RunningTaskInfo().apply { - taskId = nextTaskId++ - configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - realActivity = ComponentName(packageName, "TestActivity") - init() - } - } - - private fun createTestAppInfo( - packageName: String = "testPackageName", - className: String = "testClassName" - ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) - - private fun setInDesktopMode(inDesktopMode: Boolean) { - whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode) - } - - private companion object { - const val HOTSEAT_PACKAGE_1 = "hotseat1" - const val HOTSEAT_PACKAGE_2 = "hotseat2" - const val RUNNING_APP_PACKAGE_1 = "running1" - const val RUNNING_APP_PACKAGE_2 = "running2" - const val RUNNING_APP_PACKAGE_3 = "running3" - val ALL_APP_PACKAGES = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - RUNNING_APP_PACKAGE_3, - ) - } -} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt new file mode 100644 index 000000000..8f2679504 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -0,0 +1,1638 @@ +/* + * 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.launcher3.taskbar.bubbles.animation + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.animation.AnimatorTestRule +import androidx.core.graphics.drawable.toBitmap +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow +import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner +import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks +import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleInfo +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarViewAnimatorTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler + private lateinit var bubbleBarParentViewController: TestBubbleBarParentViewHeightUpdateNotifier + private lateinit var overflowView: BubbleView + private lateinit var bubbleView: BubbleView + private lateinit var bubble: BubbleBarBubble + private lateinit var bubbleBarView: BubbleBarView + private lateinit var flyoutContainer: FrameLayout + private lateinit var bubbleStashController: FakeBubbleStashController + private lateinit var flyoutController: BubbleBarFlyoutController + private val emptyRunnable = Runnable {} + + private val flyoutView: View? + get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + + @Before + fun setUp() { + animatorScheduler = TestBubbleBarViewAnimatorScheduler() + bubbleBarParentViewController = TestBubbleBarParentViewHeightUpdateNotifier() + bubbleStashController = FakeBubbleStashController() + PhysicsAnimatorTestUtils.prepareForTest() + setupFlyoutController() + } + + @Test + fun animateBubbleInForStashed() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // execute the hide bubble animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun animateBubbleInForStashed_tapAnimatingBubble() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + + assertThat(bubbleStashController.taskbarTouchRegionUpdated).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() } + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + } + + @Test + fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.onStashStateChangingWhileAnimating() + } + + // wait for the animation to cancel + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + handleAnimator, + DynamicAnimation.TRANSLATION_Y, + ) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleStashController.isStashed).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + @Test + fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // execute the hide bubble animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // wait for the hide animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + handleAnimator.assertIsRunning() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.onStashStateChangingWhileAnimating() + } + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + @Test + fun animateBubbleInForStashed_autoExpanding() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = true) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.isExpanded).isTrue() + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleInForStashed_expandedWhileAnimatingIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleInForStashed_expandedWhileFullyIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // wait for the animation to end + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_inApp() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedBubbleBarVisible = false + val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = onBubbleBarVisible, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.visibility = INVISIBLE + animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(handle.translationY).isEqualTo(0) + assertThat(handle.alpha).isEqualTo(1) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(notifiedBubbleBarVisible).isTrue() + + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun animateToInitialState_whileDragging_inApp() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedBubbleBarVisible = false + val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = onBubbleBarVisible, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.visibility = INVISIBLE + animator.animateToInitialState( + bubble, + isInApp = true, + isExpanding = false, + isDragging = true, + ) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(notifiedBubbleBarVisible).isTrue() + } + + @Test + fun animateToInitialState_inApp_autoExpanding() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_inHome() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + fun animateToInitialState_expandedWhileAnimatingIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true } + + bubbleBarAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_expandedWhileFullyIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + waitForFlyoutToHide() + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + barAnimator.assertIsRunning() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + // the bubble bar translation y should be back to its initial value + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + fun animateBubbleBarForCollapsed_autoExpanding() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val semaphore = Semaphore(0) + var notifiedExpanded = false + val onExpanded = Runnable { + notifiedExpanded = true + semaphore.release() + } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = true) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + // we should be expanded now + assertThat(bubbleBarView.isExpanded).isTrue() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val semaphore = Semaphore(0) + var notifiedExpanded = false + val onExpanded = Runnable { + notifiedExpanded = true + semaphore.release() + } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(100) + } + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(animator.isAnimating).isTrue() + + // send the expand signal in the middle of the lift animation + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the lift animation complete + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(150) + } + + // the lift animation is complete; the spring back animation should start now. wait for it + // to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileFullyIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + barAnimator.assertIsRunning() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait until the first frame + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // verify the hide animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + // verify the hide animation was rescheduled + assertThat(animatorScheduler.canceledBlock).isNotNull() + assertThat(animatorScheduler.delayedBlock).isNotNull() + + waitForFlyoutToFadeOutAndBackIn() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + // interrupt the animation while the flyout is collapsing + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(100) + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + + // the flyout should now reverse and expand + animatorTestRule.advanceTimeBy(400) + } + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + // verify the hide animation was rescheduled and run it + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingOut_barToHandle() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // interrupt the animation while the bar is animating to the handle + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { + bubbleBarView.alpha < 0.5 + } + // we're about to interrupt the animation which will cancel the current animation and start + // a new one. pause the scheduler to delay starting the new animation. this allows us to run + // the test deterministically + animatorScheduler.pauseScheduler = true + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + // since animation was interrupted there shouldn't be additional calls to adjust window + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // verify there's a new job scheduled and start it. this is starting the animation from the + // handle back to the bar + assertThat(animatorScheduler.pausedBlock).isNotNull() + animatorScheduler.pauseScheduler = false + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!) + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + waitForFlyoutToShow() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify the hide animation was rescheduled and run it + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptForIme() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForIme() } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + assertThat(bubbleStashController.isStashed).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + private fun setUpBubbleBar() { + bubbleBarView = BubbleBarView(context) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) + val inflater = LayoutInflater.from(context) + + val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) + overflowView = + inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView + overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) + bubbleBarView.addView(overflowView) + + val bubbleInfo = + BubbleInfo( + "key", + 0, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + bubbleView = + inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView + bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + bitmap, + bitmap, + Color.WHITE, + Path(), + "", + BubbleBarFlyoutMessage(icon = null, title = "title", message = "message"), + ) + bubbleView.setBubble(bubble) + bubbleBarView.addView(bubbleView) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } + + private fun setupFlyoutController() { + flyoutContainer = FrameLayout(context) + val flyoutPositioner = + object : BubbleBarFlyoutPositioner { + override val isOnLeft = true + override val targetTy = 100f + override val distanceToCollapsedPosition = PointF(0f, 0f) + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 10f + } + val flyoutCallbacks = + object : FlyoutCallbacks { + override fun flyoutClicked() {} + } + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController( + flyoutContainer, + flyoutPositioner, + flyoutCallbacks, + flyoutScheduler, + ) + } + + private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) { + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(ty) + assertThat(bubbleBarView.isExpanded).isTrue() + } + + private fun waitForFlyoutToShow() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(400) + } + assertThat(flyoutView).isNotNull() + } + + private fun waitForFlyoutToHide() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(350) + } + assertThat(flyoutView).isNull() + } + + private fun waitForFlyoutToFadeOutAndBackIn() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(750) + } + assertThat(flyoutView).isNotNull() + } + + private fun PhysicsAnimator.assertIsRunning() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + assertThat(isRunning()).isTrue() + } + } + + private fun PhysicsAnimator.assertIsNotRunning() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + assertThat(isRunning()).isFalse() + } + } + + private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { + + var pauseScheduler = false + var pausedBlock: Runnable? = null + private set + + var delayedBlock: Runnable? = null + private set + + var canceledBlock: Runnable? = null + private set + + override fun post(block: Runnable) { + if (pauseScheduler) { + pausedBlock = block + return + } + block.run() + } + + override fun postDelayed(delayMillis: Long, block: Runnable) { + delayedBlock = block + } + + override fun cancel(block: Runnable) { + canceledBlock = delayedBlock + delayedBlock = null + } + } + + private class TestBubbleBarParentViewHeightUpdateNotifier : + BubbleBarParentViewHeightUpdateNotifier { + + var timesInvoked: Int = 0 + + override fun updateTopBoundary() { + timesInvoked++ + } + } + + private class FakeBubbleStashController : BubbleStashController { + + var handleAnimator: PhysicsAnimator? = null + var taskbarTouchRegionUpdated = false + private set + + var animationInterrupted = false + private set + + private var _isStashed = true + + override var launcherState = BubbleLauncherState.HOME + override val isStashed: Boolean + get() = _isStashed + + override var bubbleBarVerticalCenterForHome = 0 + override var isSysuiLocked = false + override val isTransientTaskBar = true + override val hasHandleView = true + override val bubbleBarTranslationYForTaskbar = BAR_TRANSLATION_Y_FOR_TASKBAR + override val bubbleBarTranslationYForHotseat = BAR_TRANSLATION_Y_FOR_HOTSEAT + override var inAppDisplayOverrideProgress = 0f + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: BubbleStashController.ControllersAfterInitAction, + ) {} + + override fun showBubbleBarImmediate() { + _isStashed = false + } + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + _isStashed = false + } + + override fun stashBubbleBarImmediate() { + _isStashed = true + } + + override fun getTouchableHeight() = 100 + + override fun isBubbleBarVisible() = true + + override fun onNewBubbleAnimationInterrupted( + isStashed: Boolean, + bubbleBarTranslationY: Float, + ) { + _isStashed = isStashed + animationInterrupted = true + } + + override fun isEventOverBubbleBarViews(ev: MotionEvent) = false + + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {} + + override fun stashBubbleBar() { + _isStashed = true + } + + override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) { + _isStashed = false + } + + override fun getDiffBetweenHandleAndBarCenters() = DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + + override fun getStashedHandleTranslationForNewBubbleAnimation() = HANDLE_TRANSLATION + + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? { + return handleAnimator + } + + override fun updateTaskbarTouchRegion() { + taskbarTouchRegionUpdated = true + } + + override fun setHandleTranslationY(translationY: Float) {} + + override fun getHandleTranslationY() = 0f + + override fun getHandleBounds(bounds: Rect) {} + } +} + +private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f +private const val HANDLE_TRANSLATION = -30f +private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f +private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java index 44c23ba9a..c215ff222 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java @@ -24,13 +24,22 @@ import androidx.test.uiautomator.By; import androidx.test.uiautomator.Until; import com.android.launcher3.tapl.LaunchedAppState; +import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.TestUtil; +import com.android.launcher3.util.Wait; +import com.android.quickstep.fallback.RecentsState; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.views.RecentsView; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + /** * Base class for all instrumentation tests that deal with Quickstep. */ @@ -53,10 +62,48 @@ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest state) { + waitForRecentsWindowCondition(message, recentsWindow -> + recentsWindow.getStateManager().getCurrentStableState() == state.get()); + } + + // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide + // flakiness. + protected void waitForRecentsWindowCondition(String + message, Function condition) { + waitForRecentsWindowCondition(message, condition, TestUtil.DEFAULT_UI_TIMEOUT); + } + + protected T getFromRecentsWindow(Function f) { + if (!TestHelpers.isInLauncherProcess()) return null; + return getOnUiThread(() -> { + RecentsWindowManager recentsWindowManager = + RecentsWindowManager.getRecentsWindowTracker().getCreatedContext(); + return recentsWindowManager != null ? f.apply(recentsWindowManager) : null; + }); + } + + // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide + // flakiness. + protected void waitForRecentsWindowCondition( + String message, Function condition, long timeout) { + verifyKeyguardInvisible(); + if (!TestHelpers.isInLauncherProcess()) return; + Wait.atMost(message, () -> getFromRecentsWindow(condition), mLauncher, timeout); + } + + protected boolean isInRecentsWindowState(Supplier state) { + if (!TestHelpers.isInLauncherProcess()) return true; + return getFromRecentsWindow( + recentsWindow -> recentsWindow.getStateManager().getState() == state.get()); + } + protected void assertTestActivityIsRunning(int activityNumber, String message) { assertTrue(message, mDevice.wait( Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity" + activityNumber)), - DEFAULT_UI_TIMEOUT)); + TestUtil.DEFAULT_UI_TIMEOUT)); } protected LaunchedAppState getAndAssertLaunchedApp() { diff --git a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java index fc757b44c..0ccc76b24 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java @@ -55,7 +55,9 @@ public class AbstractTaplTestsTaskbar extends AbstractQuickStepTest { "com.android.launcher3.testcomponent.BaseTestingActivity"); mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder); AbstractLauncherUiTest.initialize(this); - startAppFast(CALCULATOR_APP_PACKAGE); + if (startCalendarAppDuringSetup()) { + startAppFast(CALCULATOR_APP_PACKAGE); + } mLauncher.enableBlockTimeout(true); mLauncher.showTaskbarIfHidden(); } @@ -72,8 +74,20 @@ public class AbstractTaplTestsTaskbar extends AbstractQuickStepTest { return DisplayController.isTransientTaskbar(context); } + protected boolean startCalendarAppDuringSetup() { + return true; + } + + protected boolean expectTaskbarIconsMatchHotseat() { + return true; + } + protected Taskbar getTaskbar() { Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar(); + if (!expectTaskbarIconsMatchHotseat()) { + return taskbar; + } + List taskbarIconNames = taskbar.getIconNames(); List hotseatIconNames = mLauncher.getHotseatIconNames(); diff --git a/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt new file mode 100644 index 000000000..10e85e6a1 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.content.ComponentName +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings +import android.view.Display.DEFAULT_DISPLAY +import android.view.LayoutInflater +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.logging.StatsLogManager.StatsLogger +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.launcher3.util.WindowBounds +import com.android.quickstep.orientation.LandscapePagedViewHandler +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.util.RecentsOrientedState +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.eq +import org.mockito.Mockito.isNull +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Test for [AspectRatioSystemShortcut] */ +class AspectRatioSystemShortcutTests { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + /** Spy on a concrete Context so we can reference real View, Layout, and Display properties. */ + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + + /** + * RecentsViewContainer and its super-interface ActivityContext contain methods to convert + * themselves to a Context at runtime, and static methods to convert a Context back to + * themselves by traversing ContextWrapper layers. + * + * Thus there is an undocumented assumption that a RecentsViewContainer always extends Context. + * We need to mock all of the RecentsViewContainer methods but leave the Context-under-test + * intact. + * + * The simplest way is to extend ContextWrapper and delegate the RecentsViewContainer interface + * to a mock. + */ + class RecentsViewContainerContextWrapper(base: Context) : + ContextWrapper(base), RecentsViewContainer by mock() { + + private val statsLogManager: StatsLogManager = mock() + + override fun getStatsLogManager(): StatsLogManager = statsLogManager + + override fun startActivitySafely(v: View, intent: Intent, item: ItemInfo?): RunnableList? = + null + } + + /** + * This is implicitly required in many parts of Launcher that + * require a Context. See RecentsViewContainerContextWrapper. + */ + private val launcher: RecentsViewContainerContextWrapper = + spy(RecentsViewContainerContextWrapper(context)) + + private val recentsView: LauncherRecentsView = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val taskOverlayFactory: TaskOverlayFactory = + mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private val factory: TaskShortcutFactory = + AspectRatioSystemShortcut.createFactory(abstractFloatingViewHelper) + private val statsLogger = mock() + private val orientedState: RecentsOrientedState = + mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private val taskView: TaskView = + LayoutInflater.from(context).cloneInContext(launcher).inflate(R.layout.task, null) as + TaskView + + @Before + fun setUp() { + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + + val statsLogManager = launcher.getStatsLogManager() + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + + whenever(orientedState.orientationHandler).thenReturn(LandscapePagedViewHandler()) + taskView.setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + + if (enableRefactorTaskThumbnail()) { + val recentsDependencies = RecentsDependencies.maybeInitialize(launcher) + val scopeId = recentsDependencies.createRecentsViewScope(launcher) + recentsDependencies.provide( + RecentsRotationStateRepository::class.java, + scopeId, + { mock() } + ) + recentsDependencies.provide( + RecentsDeviceProfileRepository::class.java, + scopeId, + { mock() } + ) + } + } + + @After + fun tearDown() { + if (enableRefactorTaskThumbnail()) { + RecentsDependencies.destroy(launcher) + } + } + + /** + * When the corresponding feature flag is off, there will not be an option to open aspect ratio + * settings. + */ + @DisableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_flaggedOff_notCreated() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + setScreenSizeDp(widthDp = 1200, heightDp = 800) + taskView.bind(task, orientedState, taskOverlayFactory) + + assertThat(factory.getShortcuts(launcher, taskContainer)).isNull() + } + + /** + * When the screen doesn't meet or exceed sw600dp (eg. phone, watch), there will not be an + * option to open aspect ratio settings. + */ + @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_sw599dp_notCreated() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + setScreenSizeDp(widthDp = 599, heightDp = 599) + taskView.bind(task, orientedState, taskOverlayFactory) + + assertThat(factory.getShortcuts(launcher, taskContainer)).isNull() + } + + /** + * When the screen does meet or exceed sw600dp (eg. tablet, inner foldable screen, home cinema) + * there will be an option to open aspect ratio settings. + */ + @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_sw800dp_created_andOpensSettings() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo + + setScreenSizeDp(widthDp = 1200, heightDp = 800) + taskView.bind(task, orientedState, taskOverlayFactory) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + + // On clicking the shortcut: + val shortcut = shortcuts!!.first() as AspectRatioSystemShortcut + shortcut.onClick(taskView) + + // 1) Panel should be closed + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + + // 2) Compat mode settings activity should be launched + val intentCaptor = argumentCaptor() + verify(launcher) + .startActivitySafely(any(), intentCaptor.capture(), eq(taskViewItemInfo)) + val intent = intentCaptor.firstValue!! + assertThat(intent.action).isEqualTo(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS) + + // 3) Shortcut tap event should be reported + verify(statsLogger).withItemInfo(taskViewItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_ASPECT_RATIO_SETTINGS_SYSTEM_SHORTCUT_TAP) + } + + /** + * Overrides the screen size reported in the DeviceProfile, keeping the same pixel density as + * the underlying device and adjusting the pixel width/height to match what is required. + */ + private fun setScreenSizeDp(widthDp: Int, heightDp: Int) { + val density = context.resources.configuration.densityDpi + val widthPx = widthDp * density / 160 + val heightPx = heightDp * density / 160 + + val screenBounds = WindowBounds(widthPx, heightPx, widthPx, heightPx, Surface.ROTATION_0) + val deviceProfile = + InvariantDeviceProfile.INSTANCE[context].getDeviceProfile(context) + .toBuilder(context) + .setWindowBounds(screenBounds) + .build() + whenever(launcher.getDeviceProfile()).thenReturn(deviceProfile) + } + + /** Create a (very) fake task for testing. */ + private fun createTask() = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + + /** Create TaskContainer out of a given Task and fill in the rest with mocks. */ + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, + task, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + taskOverlayFactory, + ) +} diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt index 50b5df13f..8a2393d53 100644 --- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt @@ -16,35 +16,50 @@ package com.android.quickstep +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule -import com.android.dx.mockito.inline.extended.ExtendedMockito +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.platform.app.InstrumentationRegistry import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.R import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.logging.StatsLogManager.LauncherEvent -import com.android.launcher3.model.data.WorkspaceItemInfo -import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.model.data.TaskViewItemInfo import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskThumbnailView import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskViewIcon +import com.android.quickstep.views.TaskViewType import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey import com.android.window.flags.Flags -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq @@ -54,25 +69,22 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -/** Test for DesktopSystemShortcut */ +/** Test for [DesktopSystemShortcut] */ +// TODO(b/403558856): Improve test coverage for DesktopModeCompatPolicy integration. class DesktopSystemShortcutTest { @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) - private val launcher: QuickstepLauncher = mock() + private val launcher: RecentsViewContainer = mock() private val statsLogManager: StatsLogManager = mock() private val statsLogger: StatsLogManager.StatsLogger = mock() private val recentsView: LauncherRecentsView = mock() - private val taskView: TaskView = mock() - private val workspaceItemInfo: WorkspaceItemInfo = mock() private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() - private val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = mock() - private val iconView: TaskViewIcon = mock() - private val transformingTouchDelegate: TransformingTouchDelegate = mock() + private val overlayFactory: TaskOverlayFactory = mock() private val factory: TaskShortcutFactory = DesktopSystemShortcut.createFactory(abstractFloatingViewHelper) - private val overlayFactory: TaskOverlayFactory = mock() - private val overlay: TaskOverlay<*> = mock() + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + private val taskView: TaskView = createTaskViewMock() private lateinit var mockitoSession: StaticMockitoSession @@ -81,11 +93,11 @@ class DesktopSystemShortcutTest { mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) + .mockStatic(DesktopModeStatus::class.java) .startMocking() - ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() } - ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - whenever(overlayFactory.createOverlay(any())).thenReturn(overlay) + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + whenever(launcher.asContext()).thenReturn(context) } @After @@ -95,22 +107,7 @@ class DesktopSystemShortcutTest { @Test fun createDesktopTaskShortcutFactory_desktopModeDisabled() { - setFlagsRule.disableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - - val task = - Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - isDockable = true - } - val taskContainer = createTaskContainer(task) - - val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).isNull() - } - - @Test - fun createDesktopTaskShortcutFactory_desktopModeEnabled_DeviceNotSupported() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + `when`(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) val taskContainer = createTaskContainer(createTask()) @@ -119,22 +116,139 @@ class DesktopSystemShortcutTest { } @Test - fun createDesktopTaskShortcutFactory_desktopModeEnabled_DeviceNotSupported_OverrideEnabled() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() } - - val taskContainer = spy(createTaskContainer(createTask())) - doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo - + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) + fun createDesktopTaskShortcutFactory_transparentTask() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey)) val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).isNotNull() + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION, + ) + fun createDesktopTaskShortcutFactoryPermissionEnabledAllowed_transparentTask() { + val packageManager: PackageManager = mock() + setUpTransparentPermission(packageManager, isAllowed = true) + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION, + ) + fun createDesktopTaskShortcutFactoryPermissionEnabledNotAllowed_transparentTask() { + val packageManager: PackageManager = mock() + setUpTransparentPermission(packageManager, isAllowed = false) + val baseComponent = ComponentName("", /* class */ "") + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true }) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNotEmpty() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_systemUiTask() { + val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi) + val baseComponent = ComponentName(sysUiPackageName, /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_defaultHomeTask() { + val packageManager: PackageManager = mock() + whenever(context.packageManager).thenReturn(packageManager) + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + homeActivities, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + homeActivities, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true }) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() } @Test fun createDesktopTaskShortcutFactory_undockable() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - val unDockableTask = createTask().apply { isDockable = false } val taskContainer = createTaskContainer(unDockableTask) @@ -144,8 +258,6 @@ class DesktopSystemShortcutTest { @Test fun desktopSystemShortcutClicked() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - val task = createTask() val taskContainer = spy(createTaskContainer(task)) @@ -153,17 +265,19 @@ class DesktopSystemShortcutTest { whenever(launcher.statsLogManager).thenReturn(statsLogManager) whenever(statsLogManager.logger()).thenReturn(statsLogger) whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(taskView.context).thenReturn(context) whenever(recentsView.moveTaskToDesktop(any(), any(), any())).thenAnswer { val successCallback = it.getArgument(2) successCallback.run() } - doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).hasSize(1) - assertThat(shortcuts!!.first()).isInstanceOf(DesktopSystemShortcut::class.java) + assertThat(shortcuts).isNotNull() + assertThat(shortcuts!!.single()).isInstanceOf(DesktopSystemShortcut::class.java) - val desktopShortcut = shortcuts.first() as DesktopSystemShortcut + val desktopShortcut = shortcuts.single() as DesktopSystemShortcut desktopShortcut.onClick(taskView) @@ -174,29 +288,64 @@ class DesktopSystemShortcutTest { .moveTaskToDesktop( eq(taskContainer), eq(DesktopModeTransitionSource.APP_FROM_OVERVIEW), - any() + any(), ) - verify(statsLogger).withItemInfo(workspaceItemInfo) + verify(statsLogger).withItemInfo(taskViewItemInfo) verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP) } - private fun createTask(): Task { - return Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - isDockable = true - } - } + private fun createTask() = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + .apply { isDockable = true } - private fun createTaskContainer(task: Task): TaskView.TaskContainer { - return taskView.TaskContainer( + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, task, - thumbnailView = null, - thumbnailViewDeprecated, - iconView, - transformingTouchDelegate, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, digitalWellBeingToast = null, showWindowsView = null, - overlayFactory + overlayFactory, ) + + private fun setUpTransparentPermission(packageManager: PackageManager, isAllowed: Boolean) { + val packageInfo: PackageInfo = mock() + if (isAllowed) { + packageInfo.requestedPermissions = arrayOf(SYSTEM_ALERT_WINDOW) + } + whenever(context.packageManager).thenReturn(packageManager) + whenever( + packageManager.getPackageInfoAsUser( + anyString(), + eq(PackageManager.GET_PERMISSIONS), + anyInt(), + ) + ) + .thenReturn(packageInfo) + } + + private fun createTaskViewMock(): TaskView { + val taskView: TaskView = mock() + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.context).thenReturn(context) + return taskView } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java similarity index 64% rename from quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java rename to quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java index 07d8f6199..c78fe1cf5 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java +++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java @@ -15,22 +15,29 @@ */ package com.android.quickstep; -import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.app.PendingIntent; import android.app.usage.UsageStatsManager; import android.content.Intent; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.BaseLauncherActivityTest; import com.android.quickstep.views.DigitalWellBeingToast; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import org.junit.Test; @@ -40,41 +47,42 @@ import java.time.Duration; @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { - private static final String CALCULATOR_PACKAGE = - resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); +public class DigitalWellBeingToastTest extends BaseLauncherActivityTest { + + public final String calculatorPackage = + resolveSystemAppInfo(Intent.CATEGORY_APP_CALCULATOR).packageName; @Test - public void testToast() throws Exception { - startAppFast(CALCULATOR_PACKAGE); + public void testToast() { + startAppFast(calculatorPackage); final UsageStatsManager usageStatsManager = - mTargetContext.getSystemService(UsageStatsManager.class); + targetContext().getSystemService(UsageStatsManager.class); final int observerId = 0; try { - final String[] packages = new String[]{CALCULATOR_PACKAGE}; + final String[] packages = new String[]{calculatorPackage}; // Set time limit for app. runWithShellPermission(() -> usageStatsManager.registerAppUsageLimitObserver(observerId, packages, Duration.ofSeconds(600), Duration.ofSeconds(300), - PendingIntent.getActivity(mTargetContext, -1, new Intent() - .setPackage(mTargetContext.getPackageName()), + PendingIntent.getActivity(targetContext(), -1, new Intent() + .setPackage(targetContext().getPackageName()), PendingIntent.FLAG_MUTABLE))); - mLauncher.goHome(); + loadLauncherSync(); final DigitalWellBeingToast toast = getToast(); - waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit()); - assertEquals("Toast text: ", "5 minutes left today", toast.getText()); + waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit()); + assertEquals("Toast text: ", "5 minutes left today", toast.getBannerText()); // Unset time limit for app. runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); - mLauncher.goHome(); - assertFalse("Toast is visible", getToast().hasLimit()); + goToState(LauncherState.NORMAL); + assertFalse("Toast is visible", getToast().getHasLimit()); } finally { runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); @@ -82,19 +90,20 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { } private DigitalWellBeingToast getToast() { - mLauncher.getWorkspace().switchToOverview(); + goToState(LauncherState.OVERVIEW); final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher)); return getFromLauncher(launcher -> { - TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0); - assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals( + TaskContainer taskContainer = task.getFirstTaskContainer(); + assertNotNull(taskContainer); + assertTrue("Latest task is not Calculator", calculatorPackage.equals( taskContainer.getTask().getTopComponent().getPackageName())); return taskContainer.getDigitalWellBeingToast(); }); } private TaskView getLatestTask(Launcher launcher) { - return launcher.getOverviewPanel().getTaskViewAt(0); + return launcher.getOverviewPanel().getFirstTaskView(); } private void runWithShellPermission(Runnable action) { @@ -104,6 +113,5 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { } finally { getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); } - } } diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt new file mode 100644 index 000000000..2db94f6f1 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -0,0 +1,258 @@ +/* + * 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.quickstep + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.R +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** Test for [ExternalDisplaySystemShortcut] */ +class ExternalDisplaySystemShortcutTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val launcher: RecentsViewContainer = mock() + private val statsLogManager: StatsLogManager = mock() + private val statsLogger: StatsLogManager.StatsLogger = mock() + private val recentsView: LauncherRecentsView = mock() + private val taskView: TaskView = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val overlayFactory: TaskOverlayFactory = mock() + private val factory: TaskShortcutFactory = + ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper) + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + whenever(launcher.asContext()).thenReturn(context) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun createExternalDisplayTaskShortcut_desktopModeDisabled() { + `when`(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + + val taskContainer = createTaskContainer(createTask()) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) + fun createExternalDisplayTaskShortcut_transparentTask() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_systemUiTask() { + val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi) + val baseComponent = ComponentName(sysUiPackageName, /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_defaultHomeTask() { + val packageManager: PackageManager = mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(context.packageManager).thenReturn(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + homeActivities, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + homeActivities, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true }) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun externalDisplaySystemShortcutClicked() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer { + val successCallback = it.getArgument(1) + successCallback.run() + } + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java) + + val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut + + externalDisplayShortcut.onClick(taskView) + + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any()) + verify(statsLogger).withItemInfo(taskViewItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + + private fun createTask() = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, + task, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) +} diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java index 28589291f..a4c9ef2d5 100644 --- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -19,12 +19,11 @@ import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static com.android.launcher3.Flags.enableFallbackOverviewInWindow; import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage; import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess; -import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT; import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS; -import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT; import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp; import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast; import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity; @@ -56,6 +55,7 @@ import com.android.launcher3.tapl.OverviewTask; import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.testcomponent.TestCommandReceiver; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule; import com.android.launcher3.util.rule.FailureWatcher; @@ -64,7 +64,10 @@ import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.launcher3.util.rule.TestIsolationRule; import com.android.launcher3.util.rule.TestStabilityRule; import com.android.launcher3.util.rule.ViewCaptureRule; +import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; import org.junit.After; import org.junit.Before; @@ -142,7 +145,7 @@ public class FallbackRecentsTest { }; final ViewCaptureRule viewCaptureRule = new ViewCaptureRule( - RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity); + RecentsActivity.ACTIVITY_TRACKER::getCreatedContext); mOrderSensitiveRules = RuleChain .outerRule(new SamplerRule()) .around(new TestStabilityRule()) @@ -192,29 +195,31 @@ public class FallbackRecentsTest { @Test public void goToOverviewFromApp() { startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - waitForRecentsActivityStop(); + waitForRecentsClosed(); mLauncher.getLaunchedAppState().switchToOverview(); } - protected void executeOnRecents(Consumer f) { + protected void executeOnRecents(Consumer f) { getFromRecents(r -> { f.accept(r); return true; }); } - protected T getFromRecents(Function f) { + protected T getFromRecents(Function f) { if (!TestHelpers.isInLauncherProcess()) return null; Object[] result = new Object[1]; Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> { - RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity(); - if (activity == null) { + RecentsViewContainer recentsViewContainer = enableFallbackOverviewInWindow() + ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext() + : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext(); + if (recentsViewContainer == null) { return false; } - result[0] = f.apply(activity); + result[0] = f.apply(recentsViewContainer); return true; - }).get(), DEFAULT_UI_TIMEOUT, mLauncher); + }).get(), mLauncher); return (T) result[0]; } @@ -225,14 +230,19 @@ public class FallbackRecentsTest { private void pressHomeAndWaitForOverviewClose() { mDevice.pressHome(); - waitForRecentsActivityStop(); + waitForRecentsClosed(); } - private void waitForRecentsActivityStop() { + private void waitForRecentsClosed() { try { - final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit( - () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity() == null).get(); - if (recentsActivityIsNull) { + final boolean isRecentsContainerNUll = MAIN_EXECUTOR.submit(() -> { + RecentsViewContainer recentsViewContainer = enableFallbackOverviewInWindow() + ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext() + : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext(); + + return recentsViewContainer == null; + }).get(); + if (isRecentsContainerNUll) { // Null activity counts as a "stopped" one. return; } @@ -242,9 +252,9 @@ public class FallbackRecentsTest { throw new RuntimeException(e); } - Wait.atMost("Recents activity didn't stop", + Wait.atMost("Recents view container didn't close", () -> getFromRecents(recents -> !recents.isStarted()), - DEFAULT_UI_TIMEOUT, mLauncher); + mLauncher); } @Test @@ -252,9 +262,10 @@ public class FallbackRecentsTest { startAppFast(getAppPackageName()); startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); startTestActivity(2); - waitForRecentsActivityStop(); + waitForRecentsClosed(); Wait.atMost("Expected three apps in the task list", - () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher); + () -> mLauncher.getRecentTasks().size() >= 3, + mLauncher); checkTestLauncher(); BaseOverview overview = mLauncher.getLaunchedAppState().switchToOverview(); @@ -282,7 +293,7 @@ public class FallbackRecentsTest { assertNotNull("OverviewTask.open returned null", task.open()); assertTrue("Test activity didn't open from Overview", TestHelpers.wait(Until.hasObject( By.pkg(getAppPackageName()).text("TestActivity2")), - DEFAULT_UI_TIMEOUT)); + TestUtil.DEFAULT_UI_TIMEOUT)); // Test dismissing a task. @@ -312,37 +323,39 @@ public class FallbackRecentsTest { ); } - private int getCurrentOverviewPage(RecentsActivity recents) { - return recents.getOverviewPanel().getCurrentPage(); + private int getCurrentOverviewPage(RecentsViewContainer recentsViewContainer) { + return recentsViewContainer.getOverviewPanel().getCurrentPage(); } - private int getTaskCount(RecentsActivity recents) { - return recents.getOverviewPanel().getTaskViewCount(); + private int getTaskCount(RecentsViewContainer recentsViewContainer) { + return recentsViewContainer.getOverviewPanel().getTaskViewCount(); } - private class OverviewUpdateHandler { + private class OverviewUpdateHandler implements OverviewChangeListener { - final RecentsAnimationDeviceState mRads; final OverviewComponentObserver mObserver; final CountDownLatch mChangeCounter; OverviewUpdateHandler() { Context ctx = getInstrumentation().getTargetContext(); - mRads = new RecentsAnimationDeviceState(ctx); - mObserver = new OverviewComponentObserver(ctx, mRads); + mObserver = OverviewComponentObserver.INSTANCE.get(ctx); mChangeCounter = new CountDownLatch(1); if (mObserver.getHomeIntent().getComponent() .getPackageName().equals(mOtherLauncherActivity.packageName)) { // Home already same mChangeCounter.countDown(); } else { - mObserver.setOverviewChangeListener(b -> mChangeCounter.countDown()); + mObserver.addOverviewChangeListener(this); } } + @Override + public void onOverviewTargetChange(boolean isHomeAndOverviewSame) { + mChangeCounter.countDown(); + } + void destroy() { - mObserver.onDestroy(); - mRads.destroy(); + mObserver.removeOverviewChangeListener(this); } } } diff --git a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java new file mode 100644 index 000000000..655560c0a --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java @@ -0,0 +1,635 @@ +/* + * 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.quickstep; + +import static com.android.quickstep.InputConsumerUtils.newBaseConsumer; +import static com.android.quickstep.InputConsumerUtils.newConsumer; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.StubberKt.doCallRealMethod; + +import android.annotation.NonNull; +import android.os.Looper; +import android.view.Choreographer; +import android.view.Display; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppModule; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.TaskbarManager; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; +import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.bubbles.BubbleCreator; +import com.android.launcher3.taskbar.bubbles.BubbleDismissController; +import com.android.launcher3.taskbar.bubbles.BubbleDragController; +import com.android.launcher3.taskbar.bubbles.BubblePinController; +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.util.LockedUserState; +import com.android.launcher3.util.SandboxApplication; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; +import com.android.quickstep.inputconsumers.BubbleBarInputConsumer; +import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer; +import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer; +import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer; +import com.android.quickstep.inputconsumers.OtherActivityInputConsumer; +import com.android.quickstep.inputconsumers.OverviewInputConsumer; +import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer; +import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer; +import com.android.quickstep.inputconsumers.ResetGestureInputConsumer; +import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer; +import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer; +import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer; +import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.util.NavBarPosition; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.shared.system.InputMonitorCompat; + +import dagger.BindsInstance; +import dagger.Component; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Optional; +import java.util.function.Function; + +import javax.inject.Provider; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class InputConsumerUtilsTest { + + @Rule public final SandboxApplication mContext = new SandboxApplication(); + + private final int mDisplayId = Display.DEFAULT_DISPLAY; + @NonNull private final InputMonitorCompat mInputMonitorCompat = + new InputMonitorCompat("", mDisplayId); + + private TaskAnimationManager mTaskAnimationManager; + private InputChannelCompat.InputEventReceiver mInputEventReceiver; + private boolean mUserUnlocked = true; + @NonNull private Function mSwipeUpProxyProvider = (state) -> null; + + @NonNull @Mock private TaskbarActivityContext mTaskbarActivityContext; + @NonNull @Mock private OverviewComponentObserver mOverviewComponentObserver; + @NonNull @Mock private RecentsAnimationDeviceState mDeviceState; + @NonNull @Mock private AbsSwipeUpHandler.Factory mSwipeUpHandlerFactory; + @NonNull @Mock private TaskbarManager mTaskbarManager; + @NonNull @Mock private OverviewCommandHelper mOverviewCommandHelper; + @NonNull @Mock private GestureState mPreviousGestureState; + @NonNull @Mock private GestureState mCurrentGestureState; + @NonNull @Mock private LockedUserState mLockedUserState; + @NonNull @Mock private TopTaskTracker.CachedTaskInfo mRunningTask; + @NonNull @Mock private BaseContainerInterface mContainerInterface; + @NonNull @Mock private BaseDragLayer mBaseDragLayer; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Before + public void setupTaskAnimationManager() { + mTaskAnimationManager = new TaskAnimationManager(mContext, mDeviceState, mDisplayId); + } + + @Before + public void setupDaggerGraphOverrides() { + mContext.initDaggerComponent(DaggerInputConsumerUtilsTest_TestComponent + .builder() + .bindLockedState(mLockedUserState) + .bindRotationHelper(mock(RotationTouchHelper.class)) + .bindRecentsState(mDeviceState)); + } + + @Before + public void setUpInputEventReceiver() { + runOnMainSync(() -> + mInputEventReceiver = mInputMonitorCompat.getInputReceiver( + Looper.getMainLooper(), + Choreographer.getInstance(), + event -> {})); + } + + @Before + public void setUpTaskbarActivityContext() { + NavHandle navHandle = mock(NavHandle.class); + + when(navHandle.canNavHandleBeLongPressed()).thenReturn(true); + + when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(new DeviceProfile()); + when(mTaskbarActivityContext.getNavHandle()).thenReturn(navHandle); + } + + @Before + public void setUpTaskbarManager() { + when(mTaskbarManager.getCurrentActivityContext()).thenReturn(mTaskbarActivityContext); + } + + @Before + public void setupLockedUserState() { + when(mLockedUserState.isUserUnlocked()).thenReturn(true); + } + + @Before + public void setupGestureStates() { + when(mCurrentGestureState.getRunningTask()).thenReturn(mRunningTask); + doReturn(mContainerInterface).when(mCurrentGestureState).getContainerInterface(); + } + + @Before + public void setUpContainerInterface() { + RecentsViewContainer recentsViewContainer = mock(RecentsViewContainer.class); + + when(recentsViewContainer.getDragLayer()).thenReturn(mBaseDragLayer); + when(recentsViewContainer.getRootView()).thenReturn(mBaseDragLayer); + when(recentsViewContainer.asContext()).thenReturn(mContext); + + doReturn(recentsViewContainer).when(mContainerInterface).getCreatedContainer(); + } + + @Before + public void setupBaseDragLayer() { + when(mBaseDragLayer.hasWindowFocus()).thenReturn(true); + } + + @Before + public void setupDeviceState() { + when(mDeviceState.canStartTrackpadGesture()).thenReturn(true); + when(mDeviceState.canStartSystemGesture()).thenReturn(true); + when(mDeviceState.isFullyGesturalNavMode()).thenReturn(true); + when(mDeviceState.getNavBarPosition()).thenReturn(mock(NavBarPosition.class)); + } + + @After + public void cleanUp() { + mInputMonitorCompat.dispose(); + mInputEventReceiver.dispose(); + } + + @Test + public void testNewBaseConsumer_onKeyguard_returnsDeviceLockedInputConsumer() { + when(mDeviceState.isKeyguardShowingOccluded()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + DeviceLockedInputConsumer.class, + InputConsumer.TYPE_DEVICE_LOCKED); + } + + @Test + public void testNewBaseConsumer_onLiveTileModeWithNoContainer_returnsDefaultInputConsumer() { + when(mContainerInterface.isInLiveTileMode()).thenReturn(true); + when(mContainerInterface.getCreatedContainer()).thenReturn(null); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_onLiveTileMode_returnsOverviewInputConsumer() { + when(mContainerInterface.isInLiveTileMode()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_withNoRunningTask_returnsDefaultInputConsumer() { + when(mCurrentGestureState.getRunningTask()).thenReturn(null); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_prevGestureAnimatingToLauncher_returnsOverviewInputConsumer() { + when(mPreviousGestureState.isRunningAnimationToLauncher()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_predictiveBackToHomeInProgress_returnsOverviewInputConsumer() { + when(mDeviceState.isPredictiveBackToHomeInProgress()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_resumedThroughShellTransition_returnsOverviewInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_shellNoWindowFocus_returnsOverviewWithoutFocusInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + when(mBaseDragLayer.hasWindowFocus()).thenReturn(false); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewWithoutFocusInputConsumer.class, + InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS); + } + + @Test + public void testNewBaseConsumer_forceOverviewInputConsumer_returnsOverviewInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + when(mRunningTask.isRootChooseActivity()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_launcherChildActivityResumed_returnsDefaultInputConsumer() { + when(mRunningTask.isHomeTask()).thenReturn(true); + when(mOverviewComponentObserver.isHomeAndOverviewSameActivity()).thenReturn(true); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_onGestureBlockedTask_returnsDefaultInputConsumer() { + when(mDeviceState.isGestureBlockedTask(any())).thenReturn(true); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_noGestureBlockedTask_returnsOtherActivityInputConsumer() { + doCallRealMethod().when(mDeviceState).setGestureBlockingTaskId(anyInt()); + mDeviceState.setGestureBlockingTaskId(-1); + when(mDeviceState.isGestureBlockedTask(any())).thenCallRealMethod(); + + assertCorrectInputConsumer(this::createBaseInputConsumer, OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY); + } + + @Test + public void testNewBaseConsumer_containsOtherActivityInputConsumer() { + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY); + } + + @Test + public void testNewConsumer_containsOtherActivityInputConsumer() { + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS); + } + + @Test + public void testNewConsumer_eventCanTriggerAssistantAction_containsAssistantInputConsumer() { + when(mDeviceState.canTriggerAssistantAction(any())).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ASSISTANT); + } + + @Test + public void testNewConsumer_taskbarIsPresent_containsTaskbarUnstashInputConsumer() { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.isTaskbarPresent = true; + when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(deviceProfile); + + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_TASKBAR_STASH + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_CURSOR_HOVER); + } + + @Test + public void testNewConsumer_whileSystemUiDialogShowing_returnsSysUiOverlayInputConsumer() { + when(mDeviceState.isSystemUiDialogShowing()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + SysUiOverlayInputConsumer.class, + InputConsumer.TYPE_SYSUI_OVERLAY); + } + + @Test + public void testNewConsumer_onTrackpadGesture_returnsTrackpadStatusBarInputConsumer() { + when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + TrackpadStatusBarInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_STATUS_BAR); + } + + @Test + public void testNewConsumer_whileScreenPinningActive_returnsScreenPinnedInputConsumer() { + when(mDeviceState.isScreenPinningActive()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + ScreenPinnedInputConsumer.class, + InputConsumer.TYPE_SCREEN_PINNED); + } + + @Test + public void testNewConsumer_canTriggerOneHandedAction_returnsOneHandedModeInputConsumer() { + when(mDeviceState.canTriggerOneHandedAction(any())).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + OneHandedModeInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ONE_HANDED); + } + + @Test + public void testNewConsumer_accessibilityMenuAvailable_returnsAccessibilityInputConsumer() { + when(mDeviceState.isAccessibilityMenuAvailable()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + AccessibilityInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ACCESSIBILITY); + } + + @Test + public void testNewConsumer_onStashedBubbleBar_returnsBubbleBarInputConsumer() { + BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ true); + + when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true); + when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers); + + assertCorrectInputConsumer( + this::createInputConsumer, + BubbleBarInputConsumer.class, + InputConsumer.TYPE_BUBBLE_BAR); + } + + @Test + public void testNewConsumer_onVisibleBubbleBar_returnsBubbleBarInputConsumer() { + BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ false); + + when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true); + when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers); + + assertCorrectInputConsumer( + this::createInputConsumer, + BubbleBarInputConsumer.class, + InputConsumer.TYPE_BUBBLE_BAR); + } + + @Test + public void testNewConsumer_withSwipeUpProxyProvider_returnsProgressDelegateInputConsumer() { + mSwipeUpProxyProvider = (state) -> new AnimatedFloat(); + + assertCorrectInputConsumer( + this::createInputConsumer, + ProgressDelegateInputConsumer.class, + InputConsumer.TYPE_PROGRESS_DELEGATE); + } + + @Test + public void testNewConsumer_onLockedState_returnsDeviceLockedInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + + assertCorrectInputConsumer( + this::createInputConsumer, + DeviceLockedInputConsumer.class, + InputConsumer.TYPE_DEVICE_LOCKED); + } + + @Test + public void testNewConsumer_cannotStartSysGestureOnLockedState_returnsDefaultInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + when(mDeviceState.canStartSystemGesture()).thenReturn(false); + + assertEqualsDefaultInputConsumer(this::createInputConsumer); + } + + @Test + public void testNewConsumer_cannotStartTrackGestureOnLockedState_returnsDefaultInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true); + when(mDeviceState.canStartTrackpadGesture()).thenReturn(false); + + assertEqualsDefaultInputConsumer(this::createInputConsumer); + } + + private InputConsumer createInputConsumer() { + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + InputConsumer inputConsumer = newConsumer( + mContext, + mUserUnlocked, + mOverviewComponentObserver, + mDeviceState, + mPreviousGestureState, + mCurrentGestureState, + mTaskAnimationManager, + mInputMonitorCompat, + mSwipeUpHandlerFactory, + otherActivityInputConsumer -> {}, + mInputEventReceiver, + mTaskbarManager, + mSwipeUpProxyProvider, + mOverviewCommandHelper, + event); + + event.recycle(); + + return inputConsumer; + } + + private InputConsumer createBaseInputConsumer() { + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + InputConsumer inputConsumer = newBaseConsumer( + mContext, + mUserUnlocked, + mTaskbarManager, + mOverviewComponentObserver, + mDeviceState, + mPreviousGestureState, + mCurrentGestureState, + mTaskAnimationManager, + mInputMonitorCompat, + mSwipeUpHandlerFactory, + otherActivityInputConsumer -> {}, + mInputEventReceiver, + event, + ActiveGestureLog.CompoundString.NO_OP); + + event.recycle(); + + return inputConsumer; + } + + private void assertEqualsDefaultInputConsumer( + @NonNull Provider inputConsumerProvider) { + assertCorrectInputConsumer( + inputConsumerProvider, + ResetGestureInputConsumer.class, + InputConsumer.TYPE_RESET_GESTURE); + + mUserUnlocked = false; + + assertCorrectInputConsumer( + inputConsumerProvider, + InputConsumer.class, + InputConsumer.TYPE_NO_OP); + } + + private void assertCorrectInputConsumer( + @NonNull Provider inputConsumerProvider, + @NonNull Class expectedOutputConsumer, + int expectedType) { + assertCorrectInputConsumer( + inputConsumerProvider, + expectedOutputConsumer, + expectedOutputConsumer, + expectedType); + } + + private void assertCorrectInputConsumer( + @NonNull Provider inputConsumerProvider, + @NonNull Class expectedOutputConsumer, + @NonNull Class expectedActiveConsumer, + int expectedType) { + when(mCurrentGestureState.getDisplayId()).thenReturn(mDisplayId); + + runOnMainSync(() -> { + InputConsumer inputConsumer = inputConsumerProvider.get(); + + assertThat(inputConsumer).isInstanceOf(expectedOutputConsumer); + assertThat(inputConsumer.getActiveConsumerInHierarchy()) + .isInstanceOf(expectedActiveConsumer); + assertThat(inputConsumer.getType()).isEqualTo(expectedType); + assertThat(inputConsumer.getDisplayId()).isEqualTo(mDisplayId); + }); + int expectedDisplayId = mDisplayId + 1; + + when(mCurrentGestureState.getDisplayId()).thenReturn(expectedDisplayId); + + runOnMainSync(() -> assertThat(inputConsumerProvider.get().getDisplayId()) + .isEqualTo(expectedDisplayId)); + } + + private static void runOnMainSync(@NonNull Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + private static BubbleControllers createBubbleControllers(boolean isStashed) { + BubbleBarController bubbleBarController = mock(BubbleBarController.class); + BubbleBarViewController bubbleBarViewController = mock(BubbleBarViewController.class); + BubbleStashController bubbleStashController = mock(BubbleStashController.class); + BubbleStashedHandleViewController bubbleStashedHandleViewController = + mock(BubbleStashedHandleViewController.class); + BubbleDragController bubbleDragController = mock(BubbleDragController.class); + BubbleDismissController bubbleDismissController = mock(BubbleDismissController.class); + BubbleBarPinController bubbleBarPinController = mock(BubbleBarPinController.class); + BubblePinController bubblePinController = mock(BubblePinController.class); + BubbleBarSwipeController bubbleBarSwipeController = mock(BubbleBarSwipeController.class); + BubbleCreator bubbleCreator = mock(BubbleCreator.class); + BubbleControllers bubbleControllers = new BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + Optional.of(bubbleBarSwipeController), + bubbleCreator); + + when(bubbleBarViewController.hasBubbles()).thenReturn(true); + when(bubbleStashController.isStashed()).thenReturn(isStashed); + when(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(true); + when(bubbleBarViewController.isBubbleBarVisible()).thenReturn(!isStashed); + when(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(true); + + return bubbleControllers; + } + + @LauncherAppSingleton + @Component(modules = {LauncherAppModule.class}) + interface TestComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindLockedState(LockedUserState state); + @BindsInstance Builder bindRotationHelper(RotationTouchHelper helper); + @BindsInstance Builder bindRecentsState(RecentsAnimationDeviceState state); + + @Override + TestComponent build(); + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java index 4459ed694..c713c3dc9 100644 --- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java +++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java @@ -16,7 +16,7 @@ package com.android.quickstep; -import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ALL; import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON; @@ -26,6 +26,7 @@ import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_ import android.content.Context; import android.content.pm.PackageManager; +import android.os.Process; import android.util.Log; import androidx.test.uiautomator.UiDevice; @@ -57,8 +58,6 @@ public class NavigationModeSwitchRule implements TestRule { static final String TAG = "QuickStepOnOffRule"; - public static final int WAIT_TIME_MS = 10000; - public enum Mode { THREE_BUTTON, ZERO_BUTTON, ALL } @@ -155,7 +154,9 @@ public class NavigationModeSwitchRule implements TestRule { Log.d(TAG, "setActiveOverlay: " + overlayPackage + "..."); UiDevice.getInstance(getInstrumentation()).executeShellCommand( - "cmd overlay enable-exclusive --category " + overlayPackage); + String.format("cmd overlay enable-exclusive --user %d --category %s", + Process.myUserHandle().getIdentifier(), + overlayPackage)); if (currentSysUiNavigationMode() != expectedMode) { final CountDownLatch latch = new CountDownLatch(1); @@ -179,12 +180,13 @@ public class NavigationModeSwitchRule implements TestRule { } Wait.atMost("Couldn't switch to " + overlayPackage, - () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher); + () -> launcher.getNavigationModel() == expectedMode, + launcher); Wait.atMost(() -> "Switching nav mode: " + launcher.getNavigationModeMismatchError(false), () -> launcher.getNavigationModeMismatchError(false) == null, - WAIT_TIME_MS, launcher); + launcher); AbstractLauncherUiTest.checkDetectedLeaks(launcher, false); return true; } diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java index 298dd6cae..154d86d97 100644 --- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java +++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java @@ -21,6 +21,7 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.android.launcher3.util.NavigationMode.NO_BUTTON; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -42,6 +43,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.testing.shared.ResourceUtils; +import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.RotationUtils; import com.android.launcher3.util.WindowBounds; @@ -89,7 +91,7 @@ public class OrientationTouchTransformerTest { float landscapeRegionY = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertFalse(100, landscapeRegionY, @@ -101,7 +103,8 @@ public class OrientationTouchTransformerTest { // Override region mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); tapAndAssertFalse(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertTrue(100, landscapeRegionY, @@ -112,7 +115,7 @@ public class OrientationTouchTransformerTest { event -> mTouchTransformer.touchInAssistantRegion(event)); // Override region again - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertFalse(100, landscapeRegionY, @@ -131,7 +134,8 @@ public class OrientationTouchTransformerTest { generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); tapAndAssertFalse(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertTrue(100, landscapeRegionY, @@ -143,7 +147,7 @@ public class OrientationTouchTransformerTest { // We have to add 0 rotation second so that gets set as the current rotation, otherwise // matrix transform will fail (tests only work in Portrait at the moment) mTouchTransformer.enableMultipleRegions(true, mInfo); - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -164,8 +168,9 @@ public class OrientationTouchTransformerTest { mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); - mTouchTransformer.createOrAddTouchRegion(mInfo); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(0, portraitRegionY, event -> mTouchTransformer.touchInAssistantRegion(event)); tapAndAssertFalse(0, landscapeRegionY, @@ -180,9 +185,10 @@ public class OrientationTouchTransformerTest { generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; mTouchTransformer.enableMultipleRegions(true, mInfo); - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); mTouchTransformer.enableMultipleRegions(false, mInfo); tapAndAssertTrue(0, portraitRegionY, event -> mTouchTransformer.touchInAssistantRegion(event)); @@ -212,14 +218,14 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_notInRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertFalse(100, 100, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); } @Test public void applyTransform_taskFrozen_noRotate_outOfRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); tapAndAssertFalse(100, 100, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -227,7 +233,7 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskFrozen_noRotate_inRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1; tapAndAssertTrue(100, y, @@ -236,7 +242,7 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1; tapAndAssertTrue(100, y, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -245,7 +251,8 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_90Rotate_inRegion() { mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; tapAndAssertTrue(100, y, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -253,10 +260,11 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_90Rotate_withTwoRegions() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); // Landscape point float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1); @@ -277,10 +285,11 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_90Rotate_inRotatedRegion() { // Create regions for both 0 Rotation and 90 Rotation - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); // Portrait point in landscape orientation axis float x1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0); // bottom of screen, from landscape perspective right side of screen @@ -288,6 +297,35 @@ public class OrientationTouchTransformerTest { assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY())); } + @Test + public void testSimpleOrientationTouchTransformer() { + final DisplayController displayController = mock(DisplayController.class); + doReturn(mInfo).when(displayController).getInfoForDisplay(anyInt()); + final SimpleOrientationTouchTransformer transformer = + new SimpleOrientationTouchTransformer(getApplicationContext(), displayController, + mock(DaggerSingletonTracker.class)); + final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.transform(move1, Surface.ROTATION_90); + // The position is transformed to 90 degree. + assertEquals(10, move1.getX(), 0f /* delta */); + assertEquals(NORMAL_SCREEN_SIZE.getWidth() - 100, move1.getY(), 0f /* delta */); + + // If the touching state is specified, the position is still transformed to 90 degree even + // if the given rotation is changed. + final MotionEvent move2 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.updateTouchingOrientation(Surface.ROTATION_90); + transformer.transform(move2, Surface.ROTATION_0); + assertEquals(move1.getX(), move2.getX(), 0f /* delta */); + assertEquals(move1.getY(), move2.getY(), 0f /* delta */); + + // If the touching state is cleared, it restores to use the given rotation. + final MotionEvent move3 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.clearTouchingOrientation(); + transformer.transform(move3, Surface.ROTATION_0); + assertEquals(100, move3.getX(), 0f /* delta */); + assertEquals(10, move3.getY(), 0f /* delta */); + } + private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) { Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight()); RotationUtils.rotateSize(displaySize, rotation); diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java deleted file mode 100644 index 03244eb0b..000000000 --- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep; - -import static junit.framework.TestCase.assertNull; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.KeyguardManager; - -import androidx.test.filters.SmallTest; - -import com.android.launcher3.util.LooperExecutor; -import com.android.quickstep.util.GroupTask; -import com.android.wm.shell.util.GroupedRecentTaskInfo; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@SmallTest -public class RecentTasksListTest { - - @Mock - private SystemUiProxy mockSystemUiProxy; - @Mock - private TopTaskTracker mTopTaskTracker; - - // Class under test - private RecentTasksList mRecentTasksList; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class); - KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); - mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager, - mockSystemUiProxy, mTopTaskTracker); - } - - @Test - public void onRecentTasksChanged_doesNotFetchTasks() { - mRecentTasksList.onRecentTasksChanged(); - verify(mockSystemUiProxy, times(0)) - .getRecentTasks(anyInt(), anyInt()); - } - - @Test - public void loadTasksInBackground_onlyKeys_noValidTaskDescription() { - GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks( - new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) - .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); - - List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, - true); - - assertEquals(1, taskList.size()); - assertNull(taskList.get(0).task1.taskDescription.getLabel()); - assertNull(taskList.get(0).task2.taskDescription.getLabel()); - } - - @Test - public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() { - String taskDescription = "Wheeee!"; - ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo(); - task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); - ActivityManager.RecentTaskInfo task2 = new ActivityManager.RecentTaskInfo(); - task2.taskDescription = new ActivityManager.TaskDescription(); - GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2, - null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) - .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); - - List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, - false); - - assertEquals(1, taskList.size()); - assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel()); - assertNull(taskList.get(0).task2.taskDescription.getLabel()); - } -} diff --git a/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt new file mode 100644 index 000000000..418d66c61 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.launcher3.FakeInvariantDeviceProfileTest +import com.android.quickstep.recents.data.RecentsDeviceProfile +import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl +import com.android.quickstep.views.RecentsViewContainer +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() { + private val recentsViewContainer: RecentsViewContainer = mock() + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + override fun setUp() { + super.setUp() + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(recentsViewContainer.asContext()).thenReturn(context) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun deviceProfileMappedCorrectlyForPhone() { + val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer) + initializeVarsForPhone() + val phoneDeviceProfile = newDP() + whenever(recentsViewContainer.deviceProfile).thenReturn(phoneDeviceProfile) + + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + assertThat(deviceProfileRepo.getRecentsDeviceProfile()) + .isEqualTo(RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false)) + } + + @Test + fun deviceProfileMappedCorrectlyForTablet() { + val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer) + initializeVarsForTablet() + val tabletDeviceProfile = newDP() + whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile) + + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + assertThat(deviceProfileRepo.getRecentsDeviceProfile()) + .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java index b7fd8be31..2c275f45e 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java +++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java @@ -15,20 +15,18 @@ */ package com.android.quickstep; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; - import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.content.Intent; import android.platform.test.annotations.PlatinumTest; -import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask; +import com.android.launcher3.tapl.Overview; +import com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer; import com.android.launcher3.tapl.OverviewTaskMenu; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.quickstep.util.SplitScreenTestUtils; import org.junit.Test; @@ -69,41 +67,18 @@ public class TaplOverviewIconTest extends AbstractLauncherUiTest launcher.getAppsView().resetAndScrollToPrivateSpaceHeader()); HomeAllApps homeAllApps = mLauncher.getAllApps(); diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java index 1886ce671..2fb08dd9a 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java @@ -16,6 +16,8 @@ package com.android.quickstep; +import android.util.Log; + import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -29,8 +31,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest { + public static final String TAG = "TaplStartLauncherViaGestureTests"; + static final int STRESS_REPEAT_COUNT = 10; + private enum TestCase { + TO_HOME, TO_OVERVIEW, + } + @Override @Before public void setUp() throws Exception { @@ -41,28 +49,60 @@ public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest { } @Test - @NavigationModeSwitch + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON) public void testStressPressHome() { - for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { - // Destroy Launcher activity. - closeLauncherActivity(); - - // The test action. - mLauncher.goHome(); - } + runTest(TestCase.TO_HOME); } @Test - @NavigationModeSwitch + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON) + public void testStressSwipeHome() { + runTest(TestCase.TO_HOME); + } + + @Test + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON) + public void testStressPressOverview() { + runTest(TestCase.TO_OVERVIEW); + } + + @Test + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON) public void testStressSwipeToOverview() { + runTest(TestCase.TO_OVERVIEW); + } + + private void runTest(TestCase testCase) { + long testStartTime = System.currentTimeMillis(); for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { + long loopStartTime = System.currentTimeMillis(); // Destroy Launcher activity. closeLauncherActivity(); // The test action. - mLauncher.getLaunchedAppState().switchToOverview(); + switch (testCase) { + case TO_OVERVIEW: + mLauncher.getLaunchedAppState().switchToOverview(); + break; + case TO_HOME: + mLauncher.goHome(); + break; + default: + throw new IllegalStateException("Cannot run test case: " + testCase); + } + Log.d(TAG, "Loop " + (i + 1) + " runtime=" + + (System.currentTimeMillis() - loopStartTime) + "ms"); + } + Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms"); + switch (testCase) { + case TO_OVERVIEW: + closeLauncherActivity(); + mLauncher.goHome(); + break; + case TO_HOME: + default: + // No-Op + break; } - closeLauncherActivity(); - mLauncher.goHome(); } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java index 43ebb1752..3c4f1d9c6 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java @@ -49,6 +49,7 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { DISMISS(0), LAUNCH_LAST_APP(0), LAUNCH_SELECTED_APP(1), + DISMISS_WHEN_GOING_HOME(1), LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1); private final int mNumAdditionalRunningTasks; @@ -156,6 +157,11 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { mLauncher.goHome().showQuickSwitchView().launchFocusedAppTask(CALCULATOR_APP_PACKAGE); } + @Test + public void testDismissedWhenGoingHome() { + runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS_WHEN_GOING_HOME); + } + private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) { for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) { startTestActivity(3 + i); @@ -197,6 +203,9 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE); break; + case DISMISS_WHEN_GOING_HOME: + kqs.dismissByGoingHome(); + break; case LAUNCH_OVERVIEW: kqs.moveFocusBackward(); if (!testSurface.mInitialFocusAtZero) { diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java new file mode 100644 index 000000000..b2617dd80 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME; +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.RemoteException; +import android.util.Log; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.tapl.HomeAllApps; +import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.rule.SetPropRule; +import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; +import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; +import com.android.window.flags.Flags; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; + +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.RunWith; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class TaplTestsLockedTaskbar extends AbstractTaplTestsTaskbar { + private static final String TAG = "TaplTestsLockedTaskbar"; + + @Rule + public SetPropRule mSetPropRule = + new SetPropRule(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP, "true"); + + // Default-to-desktop feature requires the display to be freeform mode. + @Rule + public ExternalResource mFreeformDisplayRule = new ExternalResource() { + private int mOriginalWindowingMode = WINDOWING_MODE_UNDEFINED; + + @Override + protected void before() { + mOriginalWindowingMode = setDisplayWindowingMode(WINDOWING_MODE_FREEFORM); + } + + @Override + protected void after() { + if (mOriginalWindowingMode != WINDOWING_MODE_UNDEFINED) { + setDisplayWindowingMode(mOriginalWindowingMode); + } + } + }; + + @Override + public void setUp() throws Exception { + Assume.assumeTrue(mLauncher.isTablet()); + Assume.assumeTrue(Flags.enterDesktopByDefaultOnFreeformDisplays()); + Assume.assumeTrue(DesktopModeStatus.canEnterDesktopMode(getTargetContext())); + super.setUp(); + } + + @Override + protected boolean startCalendarAppDuringSetup() { + return false; + } + + @Override + protected boolean expectTaskbarIconsMatchHotseat() { + return false; + } + + @Test + @PortraitLandscape + @NavigationModeSwitch + @TaskbarModeSwitch(mode = PERSISTENT) + public void testTaskbarVisibility() { + // The taskbar should be visible on home. + mDevice.pressHome(); + waitForResumed("Launcher internal state is still Background"); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + + // The taskbar should be visible when a freeform task is active. + startAppFast(CALCULATOR_APP_PACKAGE); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + } + + @Test + @PortraitLandscape + @NavigationModeSwitch + @TaskbarModeSwitch(mode = PERSISTENT) + public void testDragFromAllAppsToWorspace() { + mDevice.pressHome(); + waitForResumed("Launcher internal state is still Background"); + + final HomeAllApps allApps = getTaskbar().openAllAppsOnHome(); + allApps.freeze(); + try { + allApps.getAppIcon(TEST_APP_NAME).dragToWorkspace(false, false); + assertThat(mLauncher.getWorkspace().getWorkspaceAppIcon(TEST_APP_NAME)).isNotNull(); + } finally { + allApps.unfreeze(); + } + } + + private int setDisplayWindowingMode(int windowingMode) { + try { + int originalWindowingMode = + WindowManagerGlobal.getWindowManagerService().getWindowingMode(DEFAULT_DISPLAY); + WindowManagerGlobal.getWindowManagerService().setWindowingMode( + DEFAULT_DISPLAY, windowingMode); + return originalWindowingMode; + } catch (RemoteException e) { + Log.e(TAG, "error setting windowing mode", e); + throw new RuntimeException(e); + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt new file mode 100644 index 000000000..75947abd1 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt @@ -0,0 +1,197 @@ +/* + * 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.quickstep + +import android.platform.test.rule.AllowedDevices +import android.platform.test.rule.DeviceProduct +import android.platform.test.rule.IgnoreLimit +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.launcher3.BuildConfig +import com.android.launcher3.tapl.LaunchedAppState +import com.android.launcher3.tapl.OverviewTask +import com.android.launcher3.ui.AbstractLauncherUiTest +import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.TestUtil +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Before +import org.junit.Test + +/** Test Desktop windowing in Overview. */ +@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO]) +@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD) +class TaplTestsOverviewDesktop : AbstractLauncherUiTest() { + @Before + fun setup() { + val overview = mLauncher.goHome().switchToOverview() + if (overview.hasTasks()) { + overview.dismissAllTasks() + } + startTestAppsWithCheck() + mLauncher.goHome() + } + + @Test + @PortraitLandscape + fun enterDesktopViaOverviewMenu() { + mLauncher.workspace.switchToOverview() + moveTaskToDesktop(TEST_ACTIVITY_2) // Move last launched TEST_ACTIVITY_2 into Desktop + + // Scroll back to TEST_ACTIVITY_1, then move it into Desktop + mLauncher + .goHome() + .switchToOverview() + .apply { flingForward() } + .also { moveTaskToDesktop(TEST_ACTIVITY_1) } + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch static DesktopTaskView without live tile in Overview + val desktopTask = + mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch live-tile DesktopTaskView + desktopTask.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch static DesktopTaskView with live tile in Overview + mLauncher.goHome() + startTestActivity(TEST_ACTIVITY_EXTRA) + mLauncher.launchedAppState + .switchToOverview() + .apply { flingBackward() } + .getTestActivityTask(TEST_ACTIVITIES) + .open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + } + + @Test + @PortraitLandscape + fun dismissFocusedTasks_thenDesktopIsCentered() { + // Create DesktopTaskView + mLauncher.goHome().switchToOverview() + moveTaskToDesktop(TEST_ACTIVITY_2) + + // Create a new task activity to be the focused task + mLauncher.goHome() + startTestActivity(TEST_ACTIVITY_EXTRA) + + val overview = mLauncher.goHome().switchToOverview() + + // Dismiss focused task + val focusedTask1 = overview.currentTask + assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_EXTRA) + focusedTask1.dismiss() + + // Dismiss new focused task + val focusedTask2 = overview.currentTask + assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1) + focusedTask2.dismiss() + + // Dismiss DesktopTaskView + val desktopTask = overview.currentTask + assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue() + desktopTask.dismiss() + + assertWithMessage("Still have tasks after dismissing all the tasks") + .that(mLauncher.workspace.switchToOverview().hasTasks()) + .isFalse() + } + + @Test + @PortraitLandscape + fun dismissTasks_whenDesktopTask_IsInTheCenter() { + // Create extra activity to be DesktopTaskView + startTestActivity(TEST_ACTIVITY_EXTRA) + mLauncher.goHome().switchToOverview() + + val desktop = moveTaskToDesktop(TEST_ACTIVITY_EXTRA) + var overview = desktop.switchToOverview() + + // Open first fullscreen task and go back to Overview to validate whether it has adjacent + // tasks in its both sides (grid task on left and desktop tasks at its right side) + val firstFullscreenTaskOpened = overview.getTestActivityTask(TEST_ACTIVITY_2).open() + + // Fling to desktop task and dismiss the first fullscreen task to check repositioning of + // grid tasks. + overview = firstFullscreenTaskOpened.switchToOverview().apply { flingBackward() } + val desktopTask = overview.currentTask + assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue() + + // Get first fullscreen task (previously opened task) then dismiss this task + val firstFullscreenTaskInOverview = overview.getTestActivityTask(TEST_ACTIVITY_2) + assertTaskContentDescription(firstFullscreenTaskInOverview, TEST_ACTIVITY_2) + firstFullscreenTaskInOverview.dismiss() + + // Dismiss DesktopTask to validate whether the new task will take its position + desktopTask.dismiss() + + // Dismiss last fullscreen task + val lastFocusedTask = overview.currentTask + assertTaskContentDescription(lastFocusedTask, TEST_ACTIVITY_1) + lastFocusedTask.dismiss() + + assertWithMessage("Still have tasks after dismissing all the tasks") + .that(mLauncher.workspace.switchToOverview().hasTasks()) + .isFalse() + } + + private fun assertTaskContentDescription(task: OverviewTask, activityIndex: Int) { + assertWithMessage("The current task content description is not TestActivity$activityIndex.") + .that(task.containsContentDescription("TestActivity$activityIndex")) + .isTrue() + } + + private fun moveTaskToDesktop(activityIndex: Int): LaunchedAppState { + return mLauncher.overview + .getTestActivityTask(activityIndex) + .tapMenu() + .tapDesktopMenuItem() + .also { assertTestAppLaunched(activityIndex) } + } + + private fun startTestAppsWithCheck() { + TEST_ACTIVITIES.forEach { + startTestActivity(it) + executeOnLauncher { launcher -> + assertWithMessage( + "Launcher activity is the top activity; expecting TestActivity$it" + ) + .that(isInLaunchedApp(launcher)) + .isTrue() + } + } + } + + private fun assertTestAppLaunched(index: Int) { + assertWithMessage("TestActivity$index not opened in Desktop") + .that( + mDevice.wait( + Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")), + TestUtil.DEFAULT_UI_TIMEOUT, + ) + ) + .isTrue() + } + + companion object { + const val TEST_ACTIVITY_1 = 2 + const val TEST_ACTIVITY_2 = 3 + const val TEST_ACTIVITY_EXTRA = 4 + val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java index df73e0913..a16811efa 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java @@ -15,16 +15,12 @@ */ package com.android.quickstep; -import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; - import android.graphics.Rect; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; -import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; import org.junit.Assert; import org.junit.Test; @@ -35,8 +31,6 @@ import org.junit.runner.RunWith; public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar { @Test - @TaskbarModeSwitch(mode = PERSISTENT) - @PortraitLandscape @NavigationModeSwitch public void testTaskbarFillsWidth() { // Width check is performed inside TAPL whenever getTaskbar() is called. diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index 7877e8ac2..b207d4ac8 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -16,12 +16,14 @@ package com.android.quickstep; +import static com.android.launcher3.Flags.enableLauncherOverviewInWindow; import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT; 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.Assume.assumeFalse; @@ -30,13 +32,13 @@ import static org.junit.Assume.assumeTrue; import android.content.Intent; import android.content.res.Configuration; +import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import androidx.test.uiautomator.By; import androidx.test.uiautomator.Until; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.tapl.BaseOverview; import com.android.launcher3.tapl.LaunchedAppState; @@ -47,11 +49,13 @@ import com.android.launcher3.tapl.OverviewTask; import com.android.launcher3.tapl.SelectModeButtons; import com.android.launcher3.tapl.Workspace; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; +import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.launcher3.util.rule.TestStabilityRule; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; +import com.android.quickstep.fallback.RecentsState; import com.android.quickstep.views.RecentsView; import org.junit.After; @@ -60,6 +64,10 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Comparator; +import java.util.function.Consumer; +import java.util.function.Function; + @LargeTest @RunWith(AndroidJUnit4.class) public class TaplTestsQuickstep extends AbstractQuickStepTest { @@ -69,21 +77,32 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { private static final String READ_DEVICE_CONFIG_PERMISSION = "android.permission.READ_DEVICE_CONFIG"; + private enum ExpectedState { + + HOME(LauncherState.NORMAL, RecentsState.HOME), + OVERVIEW(LauncherState.OVERVIEW, RecentsState.DEFAULT); + + private final LauncherState mLauncherState; + private final RecentsState mRecentsState; + + ExpectedState(LauncherState launcherState, RecentsState recentsState) { + this.mLauncherState = launcherState; + this.mRecentsState = recentsState; + } + } + @Before public void setUp() throws Exception { super.setUp(); - executeOnLauncher(launcher -> { - RecentsView recentsView = launcher.getOverviewPanel(); - recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true); - }); + runOnRecentsView(recentsView -> + recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true)); } @After public void tearDown() { - executeOnLauncherInTearDown(launcher -> { - RecentsView recentsView = launcher.getOverviewPanel(); - recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false); - }); + runOnRecentsView(recentsView -> + recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false), + /* forTearDown= */ true); } public static void startTestApps() throws Exception { @@ -92,14 +111,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestActivity(2); } - private void startTestAppsWithCheck() throws Exception { - startTestApps(); - executeOnLauncher(launcher -> assertTrue( - "Launcher activity is the top activity; expecting another activity to be the top " - + "one", - isInLaunchedApp(launcher))); - } - @Test @NavigationModeSwitch @PortraitLandscape @@ -116,28 +127,26 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); // mLauncher.pressHome() also tests an important case of pressing home while in background. Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3)); + assertIsInState( + "Launcher internal state didn't switch to Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Don't have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); // Test flinging forward and backward. - executeOnLauncher(launcher -> assertEquals("Current task in Overview is not 0", - 0, getCurrentOverviewPage(launcher))); + runOnRecentsView(recentsView -> assertEquals("Current task in Overview is not 0", + 0, recentsView.getCurrentPage())); overview.flingForward(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - final Integer currentTaskAfterFlingForward = getFromLauncher( - launcher -> getCurrentOverviewPage(launcher)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", + assertIsInState("Launcher internal state is not Overview", ExpectedState.OVERVIEW); + final Integer currentTaskAfterFlingForward = + getFromRecentsView(RecentsView::getCurrentPage); + runOnRecentsView(recentsView -> assertTrue("Current task in Overview is still 0", currentTaskAfterFlingForward > 0)); overview.flingBackward(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing", - getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward)); + assertIsInState("Launcher internal state is not Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Flinging back in Overview did nothing", + recentsView.getCurrentPage() < currentTaskAfterFlingForward)); // Test opening a task. OverviewTask task = mLauncher.goHome().switchToOverview().getCurrentTask(); @@ -145,31 +154,26 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { assertNotNull("OverviewTask.open returned null", task.open()); assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject( By.pkg(getAppPackageName()).text("TestActivity2")), - DEFAULT_UI_TIMEOUT)); - executeOnLauncher(launcher -> assertTrue( - "Launcher activity is the top activity; expecting another activity to be the top " - + "one", - isInLaunchedApp(launcher))); + TestUtil.DEFAULT_UI_TIMEOUT)); + expectLaunchedAppState(); // Test dismissing a task. overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher)); + assertIsInState("Launcher internal state didn't switch to Overview", + ExpectedState.OVERVIEW); + final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount); task = overview.getCurrentTask(); assertNotNull("overview.getCurrentTask() returned null (2)", task); task.dismiss(); - executeOnLauncher( - launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview", - numTasks - 1, getTaskCount(launcher))); + runOnRecentsView(recentsView -> assertEquals( + "Dismissing a task didn't remove 1 task from Overview", + numTasks - 1, recentsView.getTaskViewCount())); // Test dismissing all tasks. mLauncher.goHome().switchToOverview().dismissAllTasks(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); - executeOnLauncher( - launcher -> assertEquals("Still have tasks after dismissing all", - 0, getTaskCount(launcher))); + assertIsInState("Launcher internal state is not Home", ExpectedState.HOME); + runOnRecentsView(recentsView -> assertEquals("Still have tasks after dismissing all", + 0, recentsView.getTaskViewCount())); } /** @@ -191,12 +195,10 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public void testDismissOverviewWithEscKey() throws Exception { startTestAppsWithCheck(); final Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state is not Overview", ExpectedState.OVERVIEW); overview.dismissByEscKey(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state is not Home", ExpectedState.HOME); } @Test @@ -217,11 +219,9 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { selectModeButtons.dismissByEscKey(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state is not Overview", ExpectedState.OVERVIEW); overview.dismissByEscKey(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state is not Home", ExpectedState.HOME); } @Test @@ -229,11 +229,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); startAppFast(CALCULATOR_APP_PACKAGE); // Ensure Calculator is last opened app. Workspace home = mLauncher.goHome(); - assertTrue("Launcher state is not Home", isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher state is not Home", ExpectedState.HOME); Overview overview = home.openOverviewFromActionPlusTabKeyboardShortcut(); - assertTrue("Launcher state is not Overview", isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher state is not Overview", ExpectedState.OVERVIEW); overview.launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE); // Assert app is focused. } @@ -242,33 +242,14 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); startAppFast(CALCULATOR_APP_PACKAGE); // Ensure Calculator is last opened app. Workspace home = mLauncher.goHome(); - assertTrue("Launcher state is not Home", isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher state is not Home", ExpectedState.HOME); Overview overview = home.openOverviewFromRecentsKeyboardShortcut(); - assertTrue("Launcher state is not Overview", isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher state is not Overview", ExpectedState.OVERVIEW); overview.launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE); // Assert app is focused. } - private int getCurrentOverviewPage(Launcher launcher) { - return launcher.getOverviewPanel().getCurrentPage(); - } - - private int getTaskCount(Launcher launcher) { - return launcher.getOverviewPanel().getTaskViewCount(); - } - - private int getTopRowTaskCountForTablet(Launcher launcher) { - return launcher.getOverviewPanel().getTopRowTaskCountForTablet(); - } - - private int getBottomRowTaskCountForTablet(Launcher launcher) { - return launcher.getOverviewPanel().getBottomRowTaskCountForTablet(); - } - - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -276,8 +257,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); assertNotNull("Workspace.switchToOverview() returned null", mLauncher.goHome().switchToOverview()); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState( + "Launcher internal state didn't switch to Overview", ExpectedState.OVERVIEW); } @Test @@ -293,9 +274,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -305,27 +283,13 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { assertNotNull("Background.switchToOverview() returned null", launchedAppState.switchToOverview()); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - } - - private void quickSwitchToPreviousAppAndAssert(boolean toRight) { - final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); - if (toRight) { - launchedAppState.quickSwitchToPreviousApp(); - } else { - launchedAppState.quickSwitchToPreviousAppSwipeLeft(); - } - - // While enable shell transition, Launcher can be resumed due to transient launch. - waitForLauncherCondition("Launcher shouldn't stay in resume forever", - this::isInLaunchedApp, 3000 /* timeout */); + assertIsInState( + "Launcher internal state didn't switch to Overview", ExpectedState.OVERVIEW); } @Test @NavigationModeSwitch @PortraitLandscape - @ScreenRecord // b/313464374 @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406 public void testQuickSwitchFromApp() throws Exception { startTestActivity(2); @@ -393,11 +357,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } - private boolean isHardwareKeyboard() { - return Configuration.KEYBOARD_QWERTY - == mTargetContext.getResources().getConfiguration().keyboard; - } - @Test @NavigationModeSwitch @PortraitLandscape @@ -418,11 +377,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { // Debug if we need to goHome to prevent wrong previous state b/315525621 mLauncher.goHome(); mLauncher.getWorkspace().switchToAllApps().pressBackToWorkspace(); - waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + waitForState("Launcher internal state didn't switch to Home", ExpectedState.HOME); startAppFast(CALCULATOR_APP_PACKAGE); mLauncher.getLaunchedAppState().pressBackToWorkspace(); - waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + waitForState("Launcher internal state didn't switch to Home", ExpectedState.HOME); } @Test @@ -437,16 +396,14 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } Overview overview = mLauncher.goHome().switchToOverview(); - executeOnLauncher( - launcher -> assertTrue("Don't have at least 13 tasks", - getTaskCount(launcher) >= 13)); + runOnRecentsView(recentsView -> assertTrue("Don't have at least 13 tasks", + recentsView.getTaskViewCount() >= 13)); // Test scroll the first task off screen overview.scrollCurrentTaskOffScreen(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", - getCurrentOverviewPage(launcher) > 0)); + assertIsInState("Launcher internal state is not Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Current task in Overview is still 0", + recentsView.getCurrentPage() > 0)); // Test opening the task. overview.getCurrentTask().open(); @@ -454,45 +411,43 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text( mLauncher.isGridOnlyOverviewEnabled() ? "TestActivity12" : "TestActivity13")), - DEFAULT_UI_TIMEOUT)); + TestUtil.DEFAULT_UI_TIMEOUT)); // Scroll the task offscreen as it is now first overview = mLauncher.goHome().switchToOverview(); overview.scrollCurrentTaskOffScreen(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", - getCurrentOverviewPage(launcher) > 0)); + assertIsInState( + "Launcher internal state is not Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Current task in Overview is still 0", + recentsView.getCurrentPage() > 0)); // Test dismissing the later task. - final Integer numTasks = getFromLauncher(this::getTaskCount); + final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount); overview.getCurrentTask().dismiss(); - executeOnLauncher( - launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview", - numTasks - 1, getTaskCount(launcher))); - executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after dismissal", - (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet( - launcher)) <= 1))); + runOnRecentsView(recentsView -> assertEquals( + "Dismissing a task didn't remove 1 task from Overview", + numTasks - 1, recentsView.getTaskViewCount())); + runOnRecentsView(recentsView -> assertTrue("Grid did not rebalance after dismissal", + (Math.abs(recentsView.getTopRowTaskCountForTablet() + - recentsView.getBottomRowTaskCountForTablet()) <= 1))); - // TODO(b/308841019): Re-enable after fixing Overview jank when dismiss -// // Test dismissing more tasks. -// assertTrue("Launcher internal state didn't remain in Overview", -// isInState(() -> LauncherState.OVERVIEW)); -// overview.getCurrentTask().dismiss(); -// assertTrue("Launcher internal state didn't remain in Overview", -// isInState(() -> LauncherState.OVERVIEW)); -// overview.getCurrentTask().dismiss(); -// executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple dismissals", -// (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet( -// launcher)) <= 1))); + // Test dismissing more tasks. + assertIsInState( + "Launcher internal state didn't remain in Overview", ExpectedState.OVERVIEW); + overview.getCurrentTask().dismiss(); + assertIsInState( + "Launcher internal state didn't remain in Overview", ExpectedState.OVERVIEW); + overview.getCurrentTask().dismiss(); + runOnRecentsView(recentsView -> assertTrue( + "Grid did not rebalance after multiple dismissals", + (Math.abs(recentsView.getTopRowTaskCountForTablet() + - recentsView.getBottomRowTaskCountForTablet()) <= 1))); // Test dismissing all tasks. mLauncher.goHome().switchToOverview().dismissAllTasks(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); - executeOnLauncher( - launcher -> assertEquals("Still have tasks after dismissing all", - 0, getTaskCount(launcher))); + assertIsInState("Launcher internal state is not Home", ExpectedState.HOME); + runOnRecentsView(recentsView -> assertEquals("Still have tasks after dismissing all", + 0, recentsView.getTaskViewCount())); } @Test @@ -501,22 +456,18 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Should have at least 3 tasks", - getTaskCount(launcher) >= 3)); + assertIsInState("Launcher internal state should be Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Should have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); // It should not dismiss overview when tapping between tasks overview.touchBetweenTasks(); overview = mLauncher.getOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", ExpectedState.OVERVIEW); // Dismiss when tapping to the right of the focused task overview.touchOutsideFirstTask(); - assertTrue("Launcher internal state should be Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Home", ExpectedState.HOME); } @Test @@ -528,34 +479,28 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Should have at least 3 tasks", - getTaskCount(launcher) >= 3)); + assertIsInState("Launcher internal state should be Overview", ExpectedState.OVERVIEW); + runOnRecentsView(recentsView -> assertTrue("Should have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); if (mLauncher.isTransientTaskbar()) { // On transient taskbar, it should dismiss when tapping outside taskbar bounds. overview.touchTaskbarBottomCorner(/* tapRight= */ false); - assertTrue("Launcher internal state should be Normal", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Normal", ExpectedState.HOME); overview = mLauncher.getWorkspace().switchToOverview(); // On transient taskbar, it should dismiss when tapping outside taskbar bounds. overview.touchTaskbarBottomCorner(/* tapRight= */ true); - assertTrue("Launcher internal state should be Normal", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Normal", ExpectedState.HOME); } else { // On persistent taskbar, it should not dismiss when tapping the taskbar overview.touchTaskbarBottomCorner(/* tapRight= */ false); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", ExpectedState.OVERVIEW); // On persistent taskbar, it should not dismiss when tapping the taskbar overview.touchTaskbarBottomCorner(/* tapRight= */ true); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", ExpectedState.OVERVIEW); } } @@ -568,7 +513,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { mLauncher.getDevice().setOrientationLeft(); startTestActivity(7); Wait.atMost("Device should not be in natural orientation", - () -> !mDevice.isNaturalOrientation(), DEFAULT_UI_TIMEOUT, mLauncher); + () -> !mDevice.isNaturalOrientation(), mLauncher); mLauncher.goHome(); } finally { mLauncher.setExpectedRotationCheckEnabled(true); @@ -581,20 +526,164 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public void testExcludeFromRecents() throws Exception { startExcludeFromRecentsTestActivity(); OverviewTask currentTask = getAndAssertLaunchedApp().switchToOverview().getCurrentTask(); - // TODO(b/326565120): the expected content description shouldn't be null but for now there - // is a bug that causes it to sometimes be for excludeForRecents tasks. assertTrue("Can't find ExcludeFromRecentsTestActivity after entering Overview from it", - currentTask.containsContentDescription("ExcludeFromRecents") - || currentTask.containsContentDescription(null)); + currentTask.containsContentDescription("ExcludeFromRecents")); // Going home should clear out the excludeFromRecents task. BaseOverview overview = mLauncher.goHome().switchToOverview(); if (overview.hasTasks()) { currentTask = overview.getCurrentTask(); assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home", - currentTask.containsContentDescription("ExcludeFromRecents") - || currentTask.containsContentDescription(null)); + currentTask.containsContentDescription("ExcludeFromRecents")); } else { // Presumably the test started with 0 tasks and remains that way after going home. } } + + @Test + @PortraitLandscape + @ScreenRecordRule.ScreenRecord // TODO(b/396447643): Remove screen record. + public void testDismissCancel() throws Exception { + startTestAppsWithCheck(); + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + ExpectedState.OVERVIEW); + final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount); + OverviewTask task = overview.getCurrentTask(); + assertNotNull("overview.getCurrentTask() returned null (2)", task); + + task.dismissCancel(); + + runOnRecentsView(recentsView -> assertEquals( + "Canceling dismissing a task removed a task from Overview", + numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount())); + } + + @Test + @PortraitLandscape + public void testDismissBottomRow() throws Exception { + assumeTrue(mLauncher.isTablet()); + mLauncher.goHome().switchToOverview().dismissAllTasks(); + startTestAppsWithCheck(); + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + ExpectedState.OVERVIEW); + final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount); + OverviewTask bottomTask = overview.getCurrentTasksForTablet().stream().max( + Comparator.comparingInt(OverviewTask::getTaskCenterY)).get(); + assertNotNull("bottomTask null", bottomTask); + + bottomTask.dismiss(); + + runOnRecentsView(recentsView -> assertEquals( + "Dismissing a bottomTask didn't remove 1 bottomTask from Overview", + numTasks - 1, recentsView.getTaskViewCount())); + } + + @Test + @PortraitLandscape + public void testDismissLastGridRow() throws Exception { + assumeTrue(mLauncher.isTablet()); + mLauncher.goHome().switchToOverview().dismissAllTasks(); + startTestAppsWithCheck(); + startTestActivity(3); + startTestActivity(4); + runOnRecentsView( + recentsView -> assertNotEquals("Grid overview should have unequal row counts", + recentsView.getTopRowTaskCountForTablet(), + recentsView.getBottomRowTaskCountForTablet())); + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + ExpectedState.OVERVIEW); + overview.flingForwardUntilClearAllVisible(); + assertTrue("Clear All not visible.", overview.isClearAllVisible()); + final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount); + OverviewTask lastGridTask = overview.getCurrentTasksForTablet().stream().min( + Comparator.comparingInt(OverviewTask::getTaskCenterX)).get(); + assertNotNull("lastGridTask null.", lastGridTask); + + lastGridTask.dismiss(); + + runOnRecentsView(recentsView -> assertEquals( + "Dismissing a lastGridTask didn't remove 1 lastGridTask from Overview", + numTasks - 1, recentsView.getTaskViewCount())); + runOnRecentsView(recentsView -> assertEquals("Grid overview should have equal row counts.", + recentsView.getTopRowTaskCountForTablet(), + recentsView.getBottomRowTaskCountForTablet())); + assertTrue("Clear All not visible.", overview.isClearAllVisible()); + } + + private void startTestAppsWithCheck() throws Exception { + startTestApps(); + expectLaunchedAppState(); + } + + private void quickSwitchToPreviousAppAndAssert(boolean toRight) { + final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); + if (toRight) { + launchedAppState.quickSwitchToPreviousApp(); + } else { + launchedAppState.quickSwitchToPreviousAppSwipeLeft(); + } + + // While enable shell transition, Launcher can be resumed due to transient launch. + waitForLauncherCondition("Launcher shouldn't stay in resume forever", + this::isInLaunchedApp, 3000 /* timeout */); + } + + private boolean isHardwareKeyboard() { + return Configuration.KEYBOARD_QWERTY + == mTargetContext.getResources().getConfiguration().keyboard; + } + + private void assertIsInState( + @NonNull String failureMessage, @NonNull ExpectedState expectedState) { + assertTrue(failureMessage, enableLauncherOverviewInWindow() + ? isInRecentsWindowState(() -> expectedState.mRecentsState) + : isInState(() -> expectedState.mLauncherState)); + } + + private void waitForState( + @NonNull String failureMessage, @NonNull ExpectedState expectedState) { + if (enableLauncherOverviewInWindow()) { + waitForRecentsWindowState(failureMessage, () -> expectedState.mRecentsState); + } else { + waitForState(failureMessage, () -> expectedState.mLauncherState); + } + } + + private void expectLaunchedAppState() { + executeOnLauncher(launcher -> assertTrue( + "Launcher activity is the top activity; expecting another activity to be the top " + + "one", + isInLaunchedApp(launcher))); + } + + private T getFromRecentsView(Function f) { + return getFromRecentsView(f, false); + } + + private T getFromRecentsView(Function f, boolean forTearDown) { + if (enableLauncherOverviewInWindow()) { + return getFromRecentsWindow(recentsWindowManager -> + (forTearDown && recentsWindowManager == null) + ? null : f.apply(recentsWindowManager.getOverviewPanel())); + } else { + return getFromLauncher(launcher -> (forTearDown && launcher == null) + ? null : f.apply(launcher.getOverviewPanel())); + } + } + + private void runOnRecentsView(Consumer f) { + runOnRecentsView(f, false); + } + + private void runOnRecentsView(Consumer f, boolean forTearDown) { + getFromRecentsView(recentsView -> { + if (forTearDown && recentsView == null) { + return null; + } + f.accept(recentsView); + return null; + }, forTearDown); + } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java index 8adf79318..37ac4a0ed 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java @@ -16,10 +16,6 @@ package com.android.quickstep; -import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; @@ -33,8 +29,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.launcher3.tapl.Overview; import com.android.launcher3.tapl.Taskbar; import com.android.launcher3.tapl.TaskbarAppIcon; -import com.android.launcher3.util.rule.TestStabilityRule; -import com.android.wm.shell.Flags; +import com.android.quickstep.util.SplitScreenTestUtils; import org.junit.After; import org.junit.Before; @@ -72,7 +67,6 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { } @Test - @TestStabilityRule.Stability(flavors = PLATFORM_POSTSUBMIT | LOCAL) // b/295225524 public void testSplitAppFromHomeWithItself() throws Exception { // Currently only tablets have Taskbar in Overview, so test is only active on tablets assumeTrue(mLauncher.isTablet()); @@ -92,28 +86,16 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { .getSplitScreenMenuItem() .click(); - if (enableSplitContextually()) { - // We're staying in all apps, use same instance - mLauncher.getAllApps() - .getAppIcon(CALCULATOR_APP_NAME) - .launchIntoSplitScreen(); - } else { - // We're in overview, use taskbar instance - mLauncher.getLaunchedAppState() - .getTaskbar() - .getAppIcon(CALCULATOR_APP_NAME) - .launchIntoSplitScreen(); - } + // We're staying in all apps, use same instance + mLauncher.getAllApps() + .getAppIcon(CALCULATOR_APP_NAME) + .launchIntoSplitScreen(); } @Test public void testSaveAppPairMenuItemOrActionExistsOnSplitPair() { - assumeTrue("App pairs feature is currently not enabled, no test needed", - Flags.enableAppPairs()); + Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher); - createAndLaunchASplitPair(); - - Overview overview = mLauncher.goHome().switchToOverview(); if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) { assertTrue("Save app pair menu item is missing", overview.getCurrentTask() @@ -124,9 +106,6 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { @Test public void testSaveAppPairMenuItemDoesNotExistOnSingleTask() throws Exception { - assumeTrue("App pairs feature is currently not enabled, no test needed", - Flags.enableAppPairs()); - startAppFast(CALCULATOR_APP_PACKAGE); assertFalse("Save app pair menu item is erroneously appearing on single task", @@ -157,24 +136,4 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { TaskbarAppIcon firstApp = taskbar.getAppIcon(firstAppName); firstApp.launchIntoSplitScreen(); } - - private void createAndLaunchASplitPair() { - clearAllRecentTasks(); - - startTestActivity(2); - startTestActivity(3); - - if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) { - mLauncher.goHome().switchToOverview().getOverviewActions() - .clickSplit() - .getTestActivityTask(2) - .open(); - } else { - mLauncher.goHome().switchToOverview().getCurrentTask() - .tapMenu() - .tapSplitMenuItem() - .getCurrentTask() - .open(); - } - } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java index 2c23f867c..710ad6f6d 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java @@ -16,8 +16,6 @@ package com.android.quickstep; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON; import static org.junit.Assert.assertNotNull; @@ -32,8 +30,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType; import com.android.launcher3.tapl.Workspace; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; -import com.android.launcher3.util.rule.ScreenRecordRule; -import com.android.launcher3.util.rule.TestStabilityRule; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.After; @@ -95,8 +91,6 @@ public class TaplTestsTrackpad extends AbstractQuickStepTest { @Test @PortraitLandscape @NavigationModeSwitch - @ScreenRecordRule.ScreenRecord // b/336606166 - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/336606166 public void switchToOverview() throws Exception { assumeTrue(mLauncher.isTablet()); diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java index 512557bf3..dc1da6934 100644 --- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java +++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java @@ -87,18 +87,6 @@ public class TaskViewTest { true); } - @Test - public void showBorderOnHoverEvent() { - mTaskView.setBorderEnabled(/* enabled= */ true); - MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0); - mTaskView.onHoverEvent(MotionEvent.obtain(event)); - verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */ - true); - mTaskView.onFocusChanged(true, 0, new Rect()); - verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */ - true); - } - @Test public void showBorderOnBorderEnabled() { presetBorderStatus(/* enabled= */ false); diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt new file mode 100644 index 000000000..daa77d2a3 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.desktop + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.util.DisplayMetrics +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import androidx.core.util.Supplier +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.app.animation.Interpolators +import com.android.internal.jank.Cuj +import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper +import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DesktopAppLaunchAnimatorHelperTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = mock() + private val resources = mock() + private val transaction = mock() + private val transactionSupplier = mock>() + + private lateinit var helper: DesktopAppLaunchAnimatorHelper + + @Before + fun setUp() { + helper = + DesktopAppLaunchAnimatorHelper( + context = context, + launchType = AppLaunchType.LAUNCH, + cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT, + transactionSupplier = transactionSupplier, + ) + whenever(transactionSupplier.get()).thenReturn(transaction) + whenever(transaction.setCrop(any(), any())).thenReturn(transaction) + whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction) + whenever(transaction.setScale(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setAlpha(any(), any())).thenReturn(transaction) + whenever(transaction.setFrameTimeline(any())).thenReturn(transaction) + + whenever(context.resources).thenReturn(resources) + whenever(resources.displayMetrics).thenReturn(DisplayMetrics()) + whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler) + } + + @Test + fun launchTransition_returnsLaunchAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(1) + assertLaunchAnimator(actual[0]) + } + + @Test + fun launchTransition_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + fun noLaunchTransition_returnsEmptyAnimatorsList() = runOnUiThread { + val pipChange = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_PIP + taskInfo = TASK_INFO_FREEFORM + } + val transitionInfo = createTransitionInfo(listOf(pipChange)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(0) + } + + @Test + fun minimizeTransition_returnsLaunchAndMinimizeAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, MINIMIZE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + } + + @Test + fun minimizeTransition_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, MINIMIZE_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertTrampolineLaunchAnimator(actual[0]) + assertCloseAnimator(actual[1]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagDisabled_returnsLaunchAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(1) + assertLaunchAnimator(actual[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo( + listOf(OPEN_CHANGE, MINIMIZE_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(3) + assertTrampolineLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + assertCloseAnimator(actual[2]) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo( + listOf(OPEN_CHANGE, MINIMIZE_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + } + + private fun assertLaunchAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator) + .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator) + assertThat(animator.childAnimations[0].duration) + .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs) + assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR) + assertThat(animator.childAnimations[1].duration) + .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs) + } + + private fun assertTrampolineLaunchAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR) + assertThat(animator.childAnimations[0].duration) + .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs) + } + + private fun assertMinimizeAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator) + .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java) + assertThat(animator.childAnimations[0].duration).isEqualTo(200) + assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[1].interpolator) + .isInstanceOf(Interpolators.LINEAR::class.java) + assertThat(animator.childAnimations[1].duration).isEqualTo(100) + } + + private fun assertCloseAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java) + assertThat(animator.duration).isEqualTo(100) + } + + private fun createTransitionInfo(changes: List): TransitionInfo { + val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0) + changes.forEach { transitionInfo.addChange(it) } + return transitionInfo + } + + private companion object { + val TASK_INFO_FREEFORM = + ActivityManager.RunningTaskInfo().apply { + baseIntent = + Intent().apply { + component = ComponentName("com.example.app", "com.example.app.MainActivity") + } + configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + } + + val OPEN_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_OPEN + taskInfo = TASK_INFO_FREEFORM + } + + val CLOSE_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_CLOSE + taskInfo = TASK_INFO_FREEFORM + } + + val MINIMIZE_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_TO_BACK + taskInfo = TASK_INFO_FREEFORM + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt new file mode 100644 index 000000000..99c74bec1 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import androidx.test.uiautomator.By +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.launcher3.tapl.Overview +import com.android.launcher3.tapl.OverviewTask +import com.android.launcher3.ui.AbstractLauncherUiTest + +object SplitScreenTestUtils { + + /** Creates 2 tasks and makes a split mode pair. Also asserts the accessibility labels. */ + @JvmStatic + fun createAndLaunchASplitPairInOverview(launcher: LauncherInstrumentation): Overview { + clearAllRecentTasks(launcher) + + AbstractLauncherUiTest.startTestActivity(2) + AbstractLauncherUiTest.startTestActivity(3) + + val overView = launcher.goHome().switchToOverview() + if (launcher.isTablet && !launcher.isGridOnlyOverviewEnabled) { + overView.overviewActions.clickSplit().getTestActivityTask(2).open() + } else { + overView.currentTask.tapMenu().tapSplitMenuItem().currentTask.open() + } + + val overviewWithSplitPair = launcher.goHome().switchToOverview() + val currentTask = overviewWithSplitPair.currentTask + currentTask.containsContentDescription( + By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(), + OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT, + ) + currentTask.containsContentDescription( + By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(), + OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT, + ) + return overviewWithSplitPair + } + + private fun clearAllRecentTasks(launcher: LauncherInstrumentation) { + if (launcher.recentTasks.isNotEmpty()) { + launcher.goHome().switchToOverview().dismissAllTasks() + } + } +} diff --git a/quickstep/tests/tests1.iml b/quickstep/tests/tests1.iml deleted file mode 100644 index f6ae8f8da..000000000 --- a/quickstep/tests/tests1.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/.DS_Store b/res/.DS_Store new file mode 100644 index 000000000..3827fa7f3 Binary files /dev/null and b/res/.DS_Store differ diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml deleted file mode 100644 index 3d7ad2bd6..000000000 --- a/res/anim-v33/shared_x_axis_activity_close_enter.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml deleted file mode 100644 index fb63602d4..000000000 --- a/res/anim-v33/shared_x_axis_activity_close_exit.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml deleted file mode 100644 index cba74ba0e..000000000 --- a/res/anim-v33/shared_x_axis_activity_open_enter.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml deleted file mode 100644 index 22e878d7f..000000000 --- a/res/anim-v33/shared_x_axis_activity_open_exit.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml deleted file mode 100644 index e54f95380..000000000 --- a/res/color-night-v31/material_color_surface_container_highest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml deleted file mode 100644 index 40f0d4c9d..000000000 --- a/res/color-night-v31/material_color_surface_container_low.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml index 24f559b49..4396f6d01 100644 --- a/res/color-night-v31/material_color_surface_container_lowest.xml +++ b/res/color-night-v31/material_color_surface_container_lowest.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml deleted file mode 100644 index ac63072a9..000000000 --- a/res/color-night-v31/material_color_surface_inverse.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml index 83822a62b..e62ed9c8a 100644 --- a/res/color-night-v31/popup_shade_first.xml +++ b/res/color-night-v31/popup_shade_first.xml @@ -12,7 +12,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/color-v31/material_color_surface_bright.xml b/res/color-v31/material_color_surface_bright.xml deleted file mode 100644 index b049851ff..000000000 --- a/res/color-v31/material_color_surface_bright.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container.xml b/res/color-v31/material_color_surface_container.xml deleted file mode 100644 index b031c081a..000000000 --- a/res/color-v31/material_color_surface_container.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml deleted file mode 100644 index a996d51eb..000000000 --- a/res/color-v31/material_color_surface_container_high.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml deleted file mode 100644 index e7a535af5..000000000 --- a/res/color-v31/material_color_surface_container_highest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml deleted file mode 100644 index b8fe01e48..000000000 --- a/res/color-v31/material_color_surface_container_low.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml index 25e866686..f726aea08 100644 --- a/res/color-v31/material_color_surface_container_lowest.xml +++ b/res/color-v31/material_color_surface_container_lowest.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml deleted file mode 100644 index e2d226fa8..000000000 --- a/res/color-v31/material_color_surface_dim.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml deleted file mode 100644 index e2d226fa8..000000000 --- a/res/color-v31/material_color_surface_variant.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml index 1278bb494..9a71caeb5 100644 --- a/res/color-v31/popup_shade_first.xml +++ b/res/color-v31/popup_shade_first.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml index 1dd8da60c..aa6c618ef 100644 --- a/res/color/overview_button.xml +++ b/res/color/overview_button.xml @@ -1,12 +1,11 @@ - + \ No newline at end of file diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml index 1278bb494..9a71caeb5 100644 --- a/res/color/popup_shade_first.xml +++ b/res/color/popup_shade_first.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml deleted file mode 100644 index 47f2a5d73..000000000 --- a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml index e279fa051..39af989a1 100644 --- a/res/drawable/add_item_dialog_background.xml +++ b/res/drawable/add_item_dialog_background.xml @@ -1,7 +1,7 @@ - + diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml index 1e7cff2e9..d200b9f96 100644 --- a/res/drawable/all_apps_tabs_background.xml +++ b/res/drawable/all_apps_tabs_background.xml @@ -13,36 +13,25 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - + - - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_selected.xml b/res/drawable/all_apps_tabs_background_selected.xml new file mode 100644 index 000000000..f7873dab3 --- /dev/null +++ b/res/drawable/all_apps_tabs_background_selected.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_selected_focused.xml b/res/drawable/all_apps_tabs_background_selected_focused.xml new file mode 100644 index 000000000..28402627f --- /dev/null +++ b/res/drawable/all_apps_tabs_background_selected_focused.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_unselected.xml b/res/drawable/all_apps_tabs_background_unselected.xml new file mode 100644 index 000000000..4004021df --- /dev/null +++ b/res/drawable/all_apps_tabs_background_unselected.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_unselected_focused.xml b/res/drawable/all_apps_tabs_background_unselected_focused.xml new file mode 100644 index 000000000..3564a0763 --- /dev/null +++ b/res/drawable/all_apps_tabs_background_unselected_focused.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_letter_list_text.xml b/res/drawable/bg_letter_list_text.xml new file mode 100644 index 000000000..073730c70 --- /dev/null +++ b/res/drawable/bg_letter_list_text.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_header.xml b/res/drawable/bg_ps_header.xml index da314452a..87a21e485 100644 --- a/res/drawable/bg_ps_header.xml +++ b/res/drawable/bg_ps_header.xml @@ -20,7 +20,7 @@ - + diff --git a/res/drawable/bg_ps_lock_button.xml b/res/drawable/bg_ps_lock_button.xml index aef1e816e..7a20e49b2 100644 --- a/res/drawable/bg_ps_lock_button.xml +++ b/res/drawable/bg_ps_lock_button.xml @@ -21,12 +21,12 @@ android:viewportHeight="36"> + android:fillColor="@color/materialColorPrimaryFixedDim"/> + android:fillColor="@color/materialColorOnPrimaryFixed"/> \ No newline at end of file diff --git a/res/drawable/bg_ps_transition_image.xml b/res/drawable/bg_ps_transition_image.xml index dfad3cf5b..694303c51 100644 --- a/res/drawable/bg_ps_transition_image.xml +++ b/res/drawable/bg_ps_transition_image.xml @@ -23,13 +23,13 @@ + android:fillColor="@color/materialColorOnPrimaryFixed"/> + android:fillColor="@color/materialColorPrimaryFixedDim"/> + android:fillColor="@color/materialColorOnPrimaryFixed"/> \ No newline at end of file diff --git a/res/drawable/bg_ps_unlock_button.xml b/res/drawable/bg_ps_unlock_button.xml index d5eedd293..563c3f6d6 100644 --- a/res/drawable/bg_ps_unlock_button.xml +++ b/res/drawable/bg_ps_unlock_button.xml @@ -21,9 +21,9 @@ android:viewportHeight="36"> + android:fillColor="@color/materialColorPrimaryFixedDim"/> \ No newline at end of file diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml index 379e0e53d..b0bd33bb5 100644 --- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml +++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml @@ -15,8 +15,7 @@ --> - + diff --git a/res/drawable/bg_widgets_header_states_two_pane.xml b/res/drawable/bg_widgets_header_states_two_pane.xml index 5f4b8c66b..1ec41a9a0 100644 --- a/res/drawable/bg_widgets_header_states_two_pane.xml +++ b/res/drawable/bg_widgets_header_states_two_pane.xml @@ -14,18 +14,16 @@ limitations under the License. --> - - - - - - + + + + - - - - - - + + + + + + diff --git a/res/drawable/bg_widgets_header_two_pane.xml b/res/drawable/bg_widgets_header_two_pane.xml index ca3feef1f..e23700289 100644 --- a/res/drawable/bg_widgets_header_two_pane.xml +++ b/res/drawable/bg_widgets_header_two_pane.xml @@ -14,13 +14,10 @@ limitations under the License. --> - - + android:insetTop="@dimen/widget_list_entry_spacing"> + - - + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml new file mode 100644 index 000000000..0ee3d1471 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml new file mode 100644 index 000000000..9028ebe27 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml new file mode 100644 index 000000000..12dc907bd --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml new file mode 100644 index 000000000..ba26f9fc3 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml index f5b68866c..723668fb3 100644 --- a/res/drawable/button_top_rounded_bordered_ripple.xml +++ b/res/drawable/button_top_rounded_bordered_ripple.xml @@ -25,7 +25,7 @@ android:topRightRadius="12dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="4dp" /> - + diff --git a/res/drawable/cloud_download_24px.xml b/res/drawable/cloud_download_24px.xml new file mode 100644 index 000000000..6f7c95aac --- /dev/null +++ b/res/drawable/cloud_download_24px.xml @@ -0,0 +1,11 @@ + + + + diff --git a/res/drawable/cloud_download_semibold_24px.xml b/res/drawable/cloud_download_semibold_24px.xml new file mode 100644 index 000000000..ef15f9f73 --- /dev/null +++ b/res/drawable/cloud_download_semibold_24px.xml @@ -0,0 +1,11 @@ + + + + diff --git a/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml new file mode 100644 index 000000000..b96a596e9 --- /dev/null +++ b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ic_aspect_ratio.xml b/res/drawable/ic_aspect_ratio.xml new file mode 100644 index 000000000..aafaac461 --- /dev/null +++ b/res/drawable/ic_aspect_ratio.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml new file mode 100644 index 000000000..1ed212e09 --- /dev/null +++ b/res/drawable/ic_bubble_button.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ic_chevron_end.xml b/res/drawable/ic_chevron_end.xml new file mode 100644 index 000000000..9ca4f3a3b --- /dev/null +++ b/res/drawable/ic_chevron_end.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/drawable/ic_chevron_start.xml b/res/drawable/ic_chevron_start.xml new file mode 100644 index 000000000..913da026d --- /dev/null +++ b/res/drawable/ic_chevron_start.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml new file mode 100644 index 000000000..24f61dd54 --- /dev/null +++ b/res/drawable/ic_close_work_edu.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml index 117258e3b..e58e1729c 100644 --- a/res/drawable/ic_corp_off.xml +++ b/res/drawable/ic_corp_off.xml @@ -16,9 +16,9 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?android:attr/textColorHint"> + android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M16,6H20C21.11,6 22,6.89 22,8V18.99C22,19.021 21.994,19.05 21.989,19.077C21.984,19.102 21.98,19.126 21.98,19.15L20,17.17V8H10.83L8,5.17V4C8,2.89 8.89,2 10,2H14C15.11,2 16,2.89 16,4V6ZM10,6H14V4H10V6ZM19,19L8,8L6,6L2.81,2.81L1.39,4.22L3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19C2,20.11 2.89,21 4,21H18.17L19.78,22.61L21.19,21.2L20.82,20.83L19,19ZM4,8V19H16.17L5.17,8H4Z" + android:fillColor="@color/materialColorOnPrimary" + android:fillType="evenOdd"/> + diff --git a/res/drawable/ic_desktop_add.xml b/res/drawable/ic_desktop_add.xml new file mode 100644 index 000000000..d31b04b85 --- /dev/null +++ b/res/drawable/ic_desktop_add.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_desktop_with_bg.xml b/res/drawable/ic_desktop_with_bg.xml new file mode 100644 index 000000000..f54285c44 --- /dev/null +++ b/res/drawable/ic_desktop_with_bg.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/res/drawable/ic_info_no_shadow.xml b/res/drawable/ic_info_no_shadow.xml index 29a81bd27..31cf51200 100644 --- a/res/drawable/ic_info_no_shadow.xml +++ b/res/drawable/ic_info_no_shadow.xml @@ -18,7 +18,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> diff --git a/res/drawable/ic_more_horiz_24.xml b/res/drawable/ic_more_horiz_24.xml new file mode 100644 index 000000000..d46827ced --- /dev/null +++ b/res/drawable/ic_more_horiz_24.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml new file mode 100644 index 000000000..c4659f821 --- /dev/null +++ b/res/drawable/ic_more_vert_dots.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_private_profile_divider_badge.xml b/res/drawable/ic_private_profile_divider_badge.xml new file mode 100644 index 000000000..92292f6bd --- /dev/null +++ b/res/drawable/ic_private_profile_divider_badge.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml new file mode 100644 index 000000000..8d125988c --- /dev/null +++ b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/res/drawable/ic_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml index cb37c9add..d66549d90 100644 --- a/res/drawable/ic_private_space_with_background.xml +++ b/res/drawable/ic_private_space_with_background.xml @@ -13,20 +13,19 @@ limitations under the License. --> + android:fillColor="@color/materialColorSurfaceContainerLowest" /> + android:fillColor="@color/materialColorOnSurface" /> + android:fillColor="@color/materialColorOnSurface" /> diff --git a/res/drawable/ic_ps_settings.xml b/res/drawable/ic_ps_settings.xml index 47edeb85e..5453f3577 100644 --- a/res/drawable/ic_ps_settings.xml +++ b/res/drawable/ic_ps_settings.xml @@ -24,9 +24,9 @@ android:pathData="M10,10h20v20h-20z"/> + android:fillColor="@color/materialColorOnSurfaceVariant"/> + android:fillColor="@color/materialColorOnSurfaceVariant"/> \ No newline at end of file diff --git a/res/drawable/ic_schedule.xml b/res/drawable/ic_schedule.xml new file mode 100644 index 000000000..d57b0a780 --- /dev/null +++ b/res/drawable/ic_schedule.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_split_horizontal.xml b/res/drawable/ic_split_horizontal.xml index 2efd2b9bc..26efedc4d 100644 --- a/res/drawable/ic_split_horizontal.xml +++ b/res/drawable/ic_split_horizontal.xml @@ -1,9 +1,25 @@ + - + android:width="20dp" + android:height="20dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + diff --git a/res/drawable/ic_split_vertical.xml b/res/drawable/ic_split_vertical.xml index 9bc97851a..787953a52 100644 --- a/res/drawable/ic_split_vertical.xml +++ b/res/drawable/ic_split_vertical.xml @@ -1,9 +1,25 @@ + - + android:width="20dp" + android:height="20dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + diff --git a/res/drawable/ic_taskbar_all_apps_button.xml b/res/drawable/ic_taskbar_all_apps_button.xml deleted file mode 100644 index 82fbbea61..000000000 --- a/res/drawable/ic_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml b/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml new file mode 100644 index 000000000..ed4a821c7 --- /dev/null +++ b/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_transient_taskbar_all_apps_button.xml b/res/drawable/ic_transient_taskbar_all_apps_button.xml deleted file mode 100644 index 6e740aed4..000000000 --- a/res/drawable/ic_transient_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml index 6200054f1..829e59078 100644 --- a/res/drawable/ic_uninstall_no_shadow.xml +++ b/res/drawable/ic_uninstall_no_shadow.xml @@ -18,7 +18,7 @@ android:height="20dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?attr/materialColorOnSurface" > + android:tint="@color/materialColorOnSurface" > diff --git a/res/drawable/ic_unpin.xml b/res/drawable/ic_unpin.xml new file mode 100644 index 000000000..557b4f9d7 --- /dev/null +++ b/res/drawable/ic_unpin.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml index 2eb1dfcde..1de111ab2 100644 --- a/res/drawable/icon_menu_arrow_background.xml +++ b/res/drawable/icon_menu_arrow_background.xml @@ -15,14 +15,13 @@ limitations under the License. --> + android:centerColor="@color/materialColorSurfaceBright" + android:endColor="@color/materialColorSurfaceBright" /> \ No newline at end of file diff --git a/res/drawable/inset_rounded_action_button.xml b/res/drawable/inset_rounded_action_button.xml new file mode 100644 index 000000000..8ae40c07b --- /dev/null +++ b/res/drawable/inset_rounded_action_button.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/res/drawable/popup_background.xml b/res/drawable/popup_background.xml index 6eedecb6c..686456f7a 100644 --- a/res/drawable/popup_background.xml +++ b/res/drawable/popup_background.xml @@ -15,6 +15,6 @@ --> - + \ No newline at end of file diff --git a/res/drawable/private_space_app_divider.xml b/res/drawable/private_space_app_divider.xml index 1ea12b332..f92dca73d 100644 --- a/res/drawable/private_space_app_divider.xml +++ b/res/drawable/private_space_app_divider.xml @@ -16,6 +16,6 @@ - + \ No newline at end of file diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml index cfec2b126..1e7fe4352 100644 --- a/res/drawable/private_space_install_app_icon.xml +++ b/res/drawable/private_space_install_app_icon.xml @@ -13,19 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - - - - - - + + + + diff --git a/res/drawable/private_space_install_app_icon_foreground.xml b/res/drawable/private_space_install_app_icon_foreground.xml new file mode 100644 index 000000000..d55abe730 --- /dev/null +++ b/res/drawable/private_space_install_app_icon_foreground.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ps_lock_background.xml b/res/drawable/ps_lock_background.xml index 0be83dbd9..bc66595ac 100644 --- a/res/drawable/ps_lock_background.xml +++ b/res/drawable/ps_lock_background.xml @@ -21,7 +21,7 @@ - + diff --git a/res/drawable/ps_settings_background.xml b/res/drawable/ps_settings_background.xml index b0c6b5b0d..7746012d4 100644 --- a/res/drawable/ps_settings_background.xml +++ b/res/drawable/ps_settings_background.xml @@ -18,6 +18,6 @@ android:inset="4dp"> - + \ No newline at end of file diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml index 81e94f7e2..6ee6d65d1 100644 --- a/res/drawable/rounded_action_button.xml +++ b/res/drawable/rounded_action_button.xml @@ -7,7 +7,7 @@ ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ - ~ Unless required by applicable law or agreed to in writing, software + ~ Unless required by applicable law or agreed to in writing, soft]ware ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and @@ -16,15 +16,11 @@ - + - + android:color="@color/materialColorSurfaceContainerLow" /> diff --git a/res/drawable/widget_picker_tabs_background.xml b/res/drawable/widget_picker_tabs_background.xml index a874dd8b9..f6607b7ad 100644 --- a/res/drawable/widget_picker_tabs_background.xml +++ b/res/drawable/widget_picker_tabs_background.xml @@ -13,36 +13,39 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/widgets_list_expand_button_background.xml b/res/drawable/widgets_list_expand_button_background.xml new file mode 100644 index 000000000..068b26db4 --- /dev/null +++ b/res/drawable/widgets_list_expand_button_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml index 4a66cacd7..0e37d4f7b 100644 --- a/res/drawable/work_card.xml +++ b/res/drawable/work_card.xml @@ -16,9 +16,8 @@ - + diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml index 6be33e86c..ad795eb47 100644 --- a/res/drawable/work_mode_fab_background.xml +++ b/res/drawable/work_mode_fab_background.xml @@ -18,7 +18,10 @@ - + + diff --git a/res/drawable/work_scheduler_background.xml b/res/drawable/work_scheduler_background.xml new file mode 100644 index 000000000..50c81837d --- /dev/null +++ b/res/drawable/work_scheduler_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml index d113a38ac..2bb2eb309 100644 --- a/res/layout/add_item_confirmation_activity.xml +++ b/res/layout/add_item_confirmation_activity.xml @@ -71,7 +71,8 @@ android:id="@+id/widget_preview_scroll_view" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginVertical="16dp" + android:layout_margin="16dp" + android:background="@drawable/widgets_surface_background" android:layout_weight="1"> + \ No newline at end of file diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml index e04b207a3..1435f8203 100644 --- a/res/layout/all_apps_personal_work_tabs.xml +++ b/res/layout/all_apps_personal_work_tabs.xml @@ -21,8 +21,6 @@ android:layout_width="match_parent" android:layout_height="@dimen/all_apps_header_pill_height" android:layout_gravity="center_horizontal" - android:paddingTop="@dimen/all_apps_tabs_vertical_padding" - android:paddingBottom="@dimen/all_apps_tabs_vertical_padding" android:layout_marginTop="@dimen/all_apps_tabs_margin_top" android:orientation="horizontal" style="@style/TextHeadline" @@ -36,6 +34,7 @@ android:layout_weight="1" android:background="@drawable/all_apps_tabs_background" android:text="@string/all_apps_personal_tab" + android:contentDescription="@string/all_apps_personal_tab_content_description" android:textColor="@color/all_apps_tab_text" android:textSize="14sp" style="?android:attr/borderlessButtonStyle" /> @@ -48,6 +47,7 @@ android:layout_weight="1" android:background="@drawable/all_apps_tabs_background" android:text="@string/all_apps_work_tab" + android:contentDescription="@string/all_apps_work_tab_content_description" android:textColor="@color/all_apps_tab_text" android:textSize="14sp" style="?android:attr/borderlessButtonStyle" /> diff --git a/quickstep/res/drawable/bg_bubble_dismiss_circle.xml b/res/layout/bubble_bar_overflow_button.xml similarity index 68% rename from quickstep/res/drawable/bg_bubble_dismiss_circle.xml rename to res/layout/bubble_bar_overflow_button.xml index b793eecdf..cb54990c8 100644 --- a/quickstep/res/drawable/bg_bubble_dismiss_circle.xml +++ b/res/layout/bubble_bar_overflow_button.xml @@ -1,6 +1,6 @@ - - - - - - - \ No newline at end of file + android:id="@+id/bubble_overflow_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> \ No newline at end of file diff --git a/res/layout/fast_scroller_letter_list_text_view.xml b/res/layout/fast_scroller_letter_list_text_view.xml new file mode 100644 index 000000000..493b6fcb7 --- /dev/null +++ b/res/layout/fast_scroller_letter_list_text_view.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml index a709fbc7a..adf45979a 100644 --- a/res/layout/launcher.xml +++ b/res/layout/launcher.xml @@ -29,6 +29,7 @@ android:importantForAccessibility="no"> - @@ -97,6 +100,7 @@ android:gravity="center_vertical" android:layout_marginStart="@dimen/ps_header_layout_margin" android:text="@string/ps_container_title" + android:maxLines="1" android:theme="@style/PrivateSpaceHeaderTextStyle" android:importantForAccessibility="no"/> diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml index 43a8aac8b..002e7b7ee 100644 --- a/res/layout/user_folder_icon_normalized.xml +++ b/res/layout/user_folder_icon_normalized.xml @@ -40,12 +40,11 @@ + android:layout_height="match_parent"> - \ No newline at end of file + diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml index 8dc785af6..7c577264d 100644 --- a/res/layout/widgets_full_sheet_paged_view.xml +++ b/res/layout/widgets_full_sheet_paged_view.xml @@ -81,6 +81,7 @@ android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:background="@drawable/widgets_surface_background" + android:clipToOutline="true" android:orientation="vertical" android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin" android:visibility="gone"> @@ -104,7 +105,6 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding" - android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding" android:layout_weight="1" android:background="@drawable/widget_picker_tabs_background" android:text="@string/widgets_full_sheet_personal_tab" @@ -117,7 +117,6 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding" - android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding" android:layout_weight="1" android:background="@drawable/widget_picker_tabs_background" android:text="@string/widgets_full_sheet_work_tab" diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml index 5427732c4..1ce1c55ec 100644 --- a/res/layout/widgets_full_sheet_recyclerview.xml +++ b/res/layout/widgets_full_sheet_recyclerview.xml @@ -64,6 +64,7 @@ android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:background="@drawable/widgets_surface_background" + android:clipToOutline="true" android:orientation="vertical" android:visibility="gone"> diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml new file mode 100644 index 000000000..ff2d777a0 --- /dev/null +++ b/res/layout/widgets_list_expand_button.xml @@ -0,0 +1,34 @@ + + +