diff --git a/Android.bp b/Android.bp index aeaeb8b2ea..4e1c9d3d11 100644 --- a/Android.bp +++ b/Android.bp @@ -17,7 +17,18 @@ 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.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,6 +42,24 @@ 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", @@ -40,6 +69,33 @@ filegroup { ], } +// 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", + ], +} + // Alternate source when quickstep is not included filegroup { name: "launcher-src_no_quickstep", @@ -63,6 +119,114 @@ 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: [ @@ -76,6 +240,7 @@ android_library { "androidx.preference_preference", "SystemUISharedLib", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", "launcher-testing-shared", ], srcs: [ @@ -136,12 +301,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,6 +321,7 @@ android_library { "//frameworks/libs/systemui:iconloader_base", "//frameworks/libs/systemui:view_capture", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", "SystemUI-statsd", "launcher-testing-shared", "androidx.lifecycle_lifecycle-common-java8", @@ -163,8 +331,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", @@ -179,6 +348,7 @@ android_library { // android_app { name: "Launcher3", + defaults: ["launcher-non-platform-apis-defaults"], static_libs: [ "Launcher3ResLib", @@ -190,7 +360,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 +368,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, @@ -233,6 +404,7 @@ android_library { "lottie", "SystemUISharedLib", "SettingsLibSettingsTheme", + "dagger2", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", @@ -241,9 +413,14 @@ android_library { // 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 +435,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", @@ -272,9 +452,14 @@ android_library { // 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: [], @@ -291,6 +476,7 @@ 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, @@ -299,10 +485,11 @@ android_library { // 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, @@ -332,13 +519,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 +534,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, @@ -372,9 +558,9 @@ android_app { include_filter: ["com.android.launcher3.*"], }, } + android_app { name: "Launcher3QuickStepGo", - static_libs: ["Launcher3GoLib"], resource_dirs: [], @@ -385,6 +571,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, diff --git a/OWNERS b/OWNERS index a66bf54b58..22efa33bf8 100644 --- a/OWNERS +++ b/OWNERS @@ -30,6 +30,7 @@ peanutbutter@google.com jeremysim@google.com atsjenk@google.com brianji@google.com +hwwang@google.com # Overview eng team alexchau@google.com @@ -52,4 +53,4 @@ per-file DeviceConfigWrapper.java, globs = set noparent per-file DeviceConfigWrapper.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com # Predictive Back -per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com \ No newline at end of file +per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com diff --git a/aconfig/Android.bp b/aconfig/Android.bp index bca7494a04..5413601382 100644 --- a/aconfig/Android.bp +++ b/aconfig/Android.bp @@ -20,7 +20,7 @@ package { aconfig_declarations { name: "com_android_launcher3_flags", package: "com.android.launcher3", - container: "system_ext", + container: "system", srcs: ["**/*.aconfig"], } diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index f1f9966f78..4d6c7ab032 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -1,5 +1,5 @@ package: "com.android.launcher3" -container: "system_ext" +container: "system" flag { name: "enable_expanding_pause_work_button" @@ -22,13 +22,6 @@ flag { bug: "316027081" } -flag { - name: "enable_grid_only_overview" - namespace: "launcher" - description: "Enable a grid-only overview without a focused task." - bug: "257950105" -} - flag { name: "enable_cursor_hover_states" namespace: "launcher" @@ -43,13 +36,6 @@ flag { bug: "302189128" } -flag { - name: "enable_overview_icon_menu" - namespace: "launcher" - description: "Enable updated overview icon and menu within task." - bug: "257950105" -} - flag { name: "enable_focus_outline" namespace: "launcher" @@ -237,13 +223,6 @@ flag { bug: "323886237" } -flag { - name: "enable_refactor_task_thumbnail" - namespace: "launcher" - description: "Enables rewritten version of TaskThumbnailViews in Overview" - bug: "331753115" -} - flag { name: "enable_handle_delayed_gesture_callbacks" namespace: "launcher" @@ -310,9 +289,140 @@ flag { } } +flag { + name: "enable_container_return_animations" + namespace: "launcher" + description: "Enables the container return animation mirroring launches." + bug: "341017746" +} + flag { name: "floating_search_bar" namespace: "launcher" description: "Search bar persists at the bottom of the screen across Launcher states" bug: "346408388" } + +flag { + name: "multiline_search_bar" + namespace: "launcher" + description: "Search bar can wrap to multi-line" + bug: "341795751" +} + +flag { + name: "enable_multi_instance_menu_taskbar" + namespace: "launcher" + description: "Menu in Taskbar with options to launch and manage multiple instances of the same app" + bug: "355237285" +} + +flag { + name: "navigate_to_child_preference" + namespace: "launcher" + description: "Settings screen supports navigating to child preference if the key is not on the screen" + bug: "293390881" +} + +flag { + name: "use_new_icon_for_archived_apps" + namespace: "launcher" + description: "Archived apps will use new cloud icon in app title instead of overlay" + bug: "350758155" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "letter_fast_scroller" + namespace: "launcher" + description: "Change fast scroller to a lettered list" + bug: "358673724" +} + +flag { + name: "enable_desktop_task_alpha_animation" + namespace: "launcher" + description: "Enables the animation of the desktop task's background view" + bug: "320307666" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "ignore_three_finger_trackpad_for_nav_handle_long_press" + namespace: "launcher" + description: "Ignore three finger trackpad event for nav handle long press" + bug: "342143522" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "one_grid_specs" + namespace: "launcher" + description: "Defines the new specs for grids based on OneGrid" + bug: "364711064" +} + +flag { + name: "one_grid_mounted_mode" + namespace: "launcher" + description: "Support a fixed landscape mode for handheld devices" + bug: "364711735" +} + +flag { + name: "one_grid_rotation_handling" + namespace: "launcher" + description: "New landscape approach for the workspace using different rows and columns in landscape and portrait" + bug: "364711814" +} + +flag { + name: "grid_migration_refactor" + namespace: "launcher" + description: "Refactor grid migration such that the code is simpler to understand and update" + bug: "358399271" +} + +flag { + name: "accessibility_scroll_on_allapps" + namespace: "launcher" + description: "Scroll to item position if accessibility focused" + bug: "265392261" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_dismiss_prediction_undo" + namespace: "launcher" + description: "Show an 'Undo' snackbar when users dismiss a predicted hotseat item" + bug: "270394476" +} + +flag { + name: "enable_all_apps_button_in_hotseat" + namespace: "launcher" + description: "Enables displaying the all apps button in the hotseat." + bug: "270393897" +} + +flag { + name: "taskbar_quiet_mode_change_support" + namespace: "launcher" + description: "Support changing quiet mode for user profiles in taskbar." + bug: "345760034" +} + +flag { + name: "taskbar_overflow" + namespace: "launcher" + description: "Show recent apps in the taskbar overflow." + bug: "368119679" +} diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig new file mode 100644 index 0000000000..853faf8167 --- /dev/null +++ b/aconfig/launcher_overview.aconfig @@ -0,0 +1,50 @@ +package: "com.android.launcher3" +container: "system" + +flag { + name: "enable_grid_only_overview" + namespace: "launcher_overview" + description: "Enable a grid-only overview without a focused task." + bug: "257950105" +} + +flag { + name: "enable_overview_icon_menu" + namespace: "launcher_overview" + description: "Enable updated overview icon and menu within task." + bug: "257950105" +} + +flag { + name: "enable_refactor_task_thumbnail" + namespace: "launcher_overview" + description: "Enables rewritten version of TaskThumbnailViews in Overview" + bug: "331753115" +} + +flag { + name: "enable_hover_of_child_elements_in_taskview" + namespace: "launcher_overview" + description: "Enables child elements to receive hover events in TaskView and respond visually to those hover events." + bug: "342594235" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_large_desktop_windowing_tile" + namespace: "launcher_overview" + description: "Makes the desktop tiles larger and moves them to the front of the list in Overview." + bug: "353947137" +} + +flag { + name: "enable_overview_command_helper_timeout" + namespace: "launcher_overview" + description: "Enables OverviewCommandHelper new version with a timeout to prevent the queue to be unresponsive." + bug: "351122926" + metadata { + purpose: PURPOSE_BUGFIX + } +} \ No newline at end of file diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig index b98eee6e32..72f654e79a 100644 --- a/aconfig/launcher_search.aconfig +++ b/aconfig/launcher_search.aconfig @@ -1,5 +1,5 @@ package: "com.android.launcher3" -container: "system_ext" +container: "system" flag { name: "enable_private_space" diff --git a/compose/facade/core/BaseComposeFacade.kt b/compose/facade/core/BaseComposeFacade.kt new file mode 100644 index 0000000000..bc7ba4700e --- /dev/null +++ b/compose/facade/core/BaseComposeFacade.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compose.core + +import android.content.Context +import android.view.View + +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 0000000000..c1cbfff0c3 --- /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 0000000000..d98a979f29 --- /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/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml index c659331bde..2524c760f4 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 ca44a6914c..8a923b511e 100644 --- a/quickstep/res/values-nl/strings.xml +++ b/quickstep/res/values-nl/strings.xml @@ -22,6 +22,7 @@ "Vastzetten" "Vrije vorm" "Desktop" + "Desktop" "Geen recente items" "Instellingen voor app-gebruik" "Alles wissen" @@ -99,7 +100,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 +131,26 @@ "Snelle instellingen" "Taakbalk" "Taakbalk wordt getoond" + "Taakbalk en bubbels links getoond" + "Taakbalk en bubbels rechts getoond" "Taakbalk is verborgen" + "Taakbalk en bubbels verborgen" "Navigatiebalk" "Taakbalk altijd tonen" "Navigatiemodus wijzigen" "Scheiding voor Taakbalk" "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.}}" + "{count,plural, =1{extra app}other{extra apps}}" + "Desktop" "%1$s en %2$s" "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" diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml index 4dc2206b70..3150ded5a0 100644 --- a/quickstep/res/values-or/strings.xml +++ b/quickstep/res/values-or/strings.xml @@ -22,6 +22,7 @@ "ପିନ୍‍" "ଫ୍ରିଫର୍ମ" "ଡେସ୍କଟପ" + "ଡେସ୍କଟପ" "ବର୍ତ୍ତମାନର କୌଣସି ଆଇଟମ ନାହିଁ" "ଆପ ବ୍ୟବହାର ସେଟିଂସ" "ସବୁ ଖାଲି କରନ୍ତୁ" @@ -99,7 +100,7 @@ "ଆପ ପେୟାର ସେଭ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" - "ବାତିଲ କରନ୍ତୁ" + "ବାତିଲ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" "ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ" @@ -130,18 +131,26 @@ "କୁଇକ ସେଟିଂସ" "ଟାସ୍କବାର" "ଟାସ୍କବାର ଦେଖାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ବାମରେ ଦେଖାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ଡାହାଣରେ ଦେଖାଯାଇଛି" "ଟାସ୍କବାର ଲୁଚାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ଲୁଚାଯାଇଛି" "ନାଭିଗେସନ ବାର" "ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ" "ନାଭିଗେସନ ମୋଡ ପରିବର୍ତ୍ତନ କରନ୍ତୁ" "ଟାସ୍କବାର ଡିଭାଇଡର" "ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ" "ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ" - "{count,plural, =1{ଅଧିକ #ଟି ଆପ ଦେଖାନ୍ତୁ।}other{ଅଧିକ #ଟି ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" - "{count,plural, =1{# ଡେସ୍କଟପ ଆପ ଦେଖାନ୍ତୁ।}other{# ଡେସ୍କଟପ ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" + "{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}" + "ଡେସ୍କଟପ" "%1$s ଏବଂ %2$s" "ବବଲ" "ଓଭରଫ୍ଲୋ" "%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 fc603969a7..7da7555e2b 100644 --- a/quickstep/res/values-pa/strings.xml +++ b/quickstep/res/values-pa/strings.xml @@ -22,6 +22,7 @@ "ਪਿੰਨ ਕਰੋ" "ਫ੍ਰੀਫਾਰਮ" "ਡੈਸਕਟਾਪ" + "ਡੈਸਕਟਾਪ" "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮ ਨਹੀਂ" "ਐਪ ਵਰਤੋਂ ਦੀਆਂ ਸੈਟਿੰਗਾਂ" "ਸਭ ਕਲੀਅਰ ਕਰੋ" @@ -99,7 +100,7 @@ "ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" - "ਰੱਦ ਕਰੋ" + "ਰੱਦ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" "ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ" @@ -130,18 +131,26 @@ "ਤਤਕਾਲ ਸੈਟਿੰਗਾਂ" "ਟਾਸਕਬਾਰ" "ਟਾਸਕਬਾਰ ਨੂੰ ਦਿਖਾਇਆ ਗਿਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਖੱਬੇ ਦਿਖਾਇਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਸੱਜੇ ਦਿਖਾਇਆ" "ਟਾਸਕਬਾਰ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ" + "ਟਾਸਕਬਾਰ ਅਤੇ ਬਬਲ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ" "ਨੈਵੀਗੇਸ਼ਨ ਵਾਲੀ ਪੱਟੀ" "ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਓ" "ਨੈਵੀਗੇਸ਼ਨ ਮੋਡ ਬਦਲੋ" "ਟਾਸਕਬਾਰ ਵਿਭਾਜਕ" "ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" "ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" - "{count,plural, =1{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}one{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}other{# ਹੋਰ ਐਪਾਂ ਦਿਖਾਓ।}}" - "{count,plural, =1{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}one{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}other{# ਡੈਸਕਟਾਪ ਐਪਾਂ ਦਿਖਾਓ।}}" + "{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}" + "ਡੈਸਕਟਾਪ" "%1$s ਅਤੇ %2$s" "ਬਬਲ" "ਓਵਰਫ਼ਲੋ" "%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 13c8c1c48b..0cc49e263e 100644 --- a/quickstep/res/values-pl/strings.xml +++ b/quickstep/res/values-pl/strings.xml @@ -22,6 +22,7 @@ "Przypnij" "Tryb dowolny" "Pulpit" + "Pulpit" "Brak ostatnich elementów" "Ustawienia użycia aplikacji" "Wyczyść wszystko" @@ -99,7 +100,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 +131,26 @@ "Szybkie ustawienia" "Pasek aplikacji" "Pasek aplikacji widoczny" + "Pasek i dymki po lewej" + "Pasek i dymki po prawej" "Pasek aplikacji ukryty" + "Pasek i dymki są ukryte" "Pasek nawigacyjny" "Zawsze pokazuj pasek aplikacji" "Zmień tryb nawigacji" "Linia dzielenia paska 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.}}" + "{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}" + "Pulpit" "%1$s%2$s" "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" diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml index b77f598227..33b87df643 100644 --- a/quickstep/res/values-pt-rPT/strings.xml +++ b/quickstep/res/values-pt-rPT/strings.xml @@ -22,6 +22,7 @@ "Fixar" "Forma livre" "Computador" + "Computador" "Nenhum item recente" "Definições de utilização de aplicações" "Limpar tudo" @@ -99,7 +100,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 +131,26 @@ "Definiç. rápidas" "Barra de tarefas" "Barra de tarefas apresentada" + "Barra de tarefas/balões à esq." + "Barra de tarefas/balões à dir." "Barra de tarefas ocultada" + "Barra tarefas/balões ocultos" "Barra de navegação" "Ver sempre Barra de tarefas" "Alterar modo de navegação" "Divisor 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.}}" + "{count,plural, =1{outra app}other{outras apps}}" + "Computador" "%1$s e %2$s" "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" diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml index 2b051476ee..0aa629511d 100644 --- a/quickstep/res/values-pt/strings.xml +++ b/quickstep/res/values-pt/strings.xml @@ -22,6 +22,7 @@ "Fixar" "Forma livre" "Modo área de trabalho" + "Computador" "Nenhum item recente" "Configurações de uso do app" "Remover tudo" @@ -99,7 +100,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" @@ -130,18 +131,26 @@ "Config. rápidas" "Barra de tarefas" "Barra de tarefas visível" + "Barra de tar. e balões à esq." + "Barra de tar. e balões à dir." "Barra de tarefas oculta" + "Barra de tar. e balões ocultos" "Barra de navegação" "Sempre mostrar a Barra de tarefas" "Mudar o modo de navegação" "Separador da Barra de tarefas" "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.}}" + "{count,plural, =1{outro app}one{outro app}other{outros apps}}" + "Computador" "%1$s e %2$s" "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" diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml index 225bccf2a2..2f9d287535 100644 --- a/quickstep/res/values-ro/strings.xml +++ b/quickstep/res/values-ro/strings.xml @@ -22,6 +22,7 @@ "Fixează" "Formă liberă" "Desktop" + "Computer" "Niciun element recent" "Setări de utilizare a aplicației" "Șterge tot" @@ -99,7 +100,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 +131,26 @@ "Setări rapide" "Bară de activități" "Bara de activități este afișată" + "Bară și baloane stânga afișate" + "Bară & baloane dreapta afișate" "Bara de activități este ascunsă" + "Bară și baloane ascunse" "Bară de navigare" "Afișează mereu bara" "Schimbă modul de navigare" "Separator 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.}}" + "{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}" + "Computer" "%1$s și %2$s" "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" diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml index c9800383f1..2722ca9464 100644 --- a/quickstep/res/values-ru/strings.xml +++ b/quickstep/res/values-ru/strings.xml @@ -22,6 +22,7 @@ "Закрепить" "Произвольная форма" "Мультиоконный режим" + "Мультиоконный режим" "Здесь пока ничего нет." "Настройки использования приложения" "Очистить все" @@ -99,7 +100,7 @@ "Сохранить приложения" "Для разделения экрана выберите другое приложение." "Чтобы использовать разделенный экран, выберите другое приложение." - "Отмена" + "Отмена" "Выйдите из режима разделения экрана." "Выберите другое приложение для разделения экрана." "Это действие заблокировано приложением или организацией." @@ -130,18 +131,26 @@ "Быстрые настройки" "Панель задач" "Панель задач показана" + "Слева панель задач, подсказки" + "Справа панель задач, подсказки" "Панель задач скрыта" + "Скрыты панель задач, подсказки" "Панель навигации" "Всегда показывать панель задач" "Изменить режим навигации" "Разделитель панели задач" "Переместить вверх или влево" "Переместить вниз или вправо" - "{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$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 9cbe837f94..19b61f7dd0 100644 --- a/quickstep/res/values-si/strings.xml +++ b/quickstep/res/values-si/strings.xml @@ -22,6 +22,7 @@ "අමුණන්න" "Freeform" "ඩෙස්ක්ටොපය" + "ඩෙස්ක්ටොපය" "මෑත අයිතම නැත" "යෙදුම් භාවිත සැකසීම්" "සියල්ල හිස් කරන්න" @@ -99,7 +100,7 @@ "යෙදුම් යුගල සුරකින්න" "බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න" "බෙදුම් තිරය භාවිත කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" - "අවලංගු කරන්න" + "අවලංගු කරන්න" "බෙදීම් තිර තේරීමෙන් පිටවන්න" "බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" "මෙම ක්‍රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ" @@ -130,18 +131,26 @@ "ඉක්මන් සැකසීම්" "කාර්ය තීරුව" "කාර්ය තීරුව පෙන්වා ඇත" + "කාර්ය තීරුව සහ බුබුළු පෙන්වා ඇත" + "කාර්ය තීරුව සහ බුබුළු දකුණට පෙන්වා ඇත" "කාර්ය තීරුව සඟවා ඇත" + "කාර්ය තීරුව සහ බුබුළු සඟවා ඇත" "සංචලන තීරුව" "සෑම විටම කාර්ය තීරුව පෙන්වන්න" "සංචාලන ප්‍රකාරය වෙනස් කරන්න" "කාර්ය තීරු බෙදනය" "ඉහළ/වම වෙත ගෙන යන්න" "පහළ/දකුණ වෙත ගෙන යන්න" - "{count,plural, =1{තවත් # යෙදුමක් පෙන්වන්න.}one{තවත් යෙදුම් #ක් පෙන්වන්න.}other{තවත් යෙදුම් #ක් පෙන්වන්න.}}" - "{count,plural, =1{# ඩෙස්ක්ටොප් යෙදුමක් පෙන්වන්න.}one{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}other{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}}" + "{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}" + "ඩෙස්ක්ටොපය" "%1$s සහ %2$s" "බුබුළු" "පිටාර යාම" "%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 20a2f99136..0f0ec278e3 100644 --- a/quickstep/res/values-sk/strings.xml +++ b/quickstep/res/values-sk/strings.xml @@ -22,6 +22,7 @@ "Pripnúť" "Voľný režim" "Počítač" + "Počítač" "Žiadne nedávne položky" "Nastavenia využívania aplikácie" "Vymazať všetko" @@ -99,7 +100,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 +131,26 @@ "Rýchle nastavenia" "Panel aplikácií" "Panel aplikácií je zobrazený" + "Panel aplik. a bubl. sú vľavo" + "Panel aplik. a bubl. sú vpravo" "Panel aplikácií je skrytý" + "Panel aplik. a bubl. sú skryté" "Navigačný panel" "Zobrazovať panel aplikácií" "Zmeniť režim navigácie" "Rozdeľovač 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č.}}" + "{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%2$s" "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" diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml index 52faeb7a9e..972ced5410 100644 --- a/quickstep/res/values-sl/strings.xml +++ b/quickstep/res/values-sl/strings.xml @@ -22,6 +22,7 @@ "Pripni" "Prosta oblika" "Namizni računalnik" + "Namizni način" "Ni nedavnih elementov" "Nastavitve uporabe aplikacij" "Počisti vse" @@ -99,7 +100,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 +131,26 @@ "Hitre nastavitve" "Opravilna vrstica" "Opravilna vrstica je prikazana" + "Prikazani so opravilna vrstica in oblački na levi" + "Prikazani so opravilna vrstica in oblački na desni" "Opravilna vrstica je skrita" + "Opravilna vrstica in oblački so skriti" "Vrstica za krmarjenje" "Stalen prikaz oprav. vrstice" "Spreminjanje načina navigacije" "Razdelilnik opravilne vrstice" "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.}}" + "{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" "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" diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml index cdb9cf9a7c..d2a72811d9 100644 --- a/quickstep/res/values-sq/strings.xml +++ b/quickstep/res/values-sq/strings.xml @@ -22,6 +22,7 @@ "Gozhdo" "Formë e lirë" "Desktopi" + "Desktopi" "Nuk ka asnjë artikull të fundit" "Cilësimet e përdorimit të aplikacionit" "Pastroji të gjitha" @@ -99,7 +100,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 +131,26 @@ "Cilësimet shpejt" "Shiriti i detyrave" "Shiriti i detyrave i shfaqur" + "Shiriti i detyrave dhe flluskat majtas janë shfaqur" + "Shiriti i detyrave dhe flluskat djathtas janë shfaqur" "Shiriti i detyrave i fshehur" + "Shiriti i detyrave dhe flluskat janë fshehur" "Shiriti i navigimit" "Shfaq gjithmonë shiritin e detyrave" "Ndrysho modalitetin e navigimit" "Ndarësi 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.}}" + "{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}" + "Desktop" "%1$s dhe %2$s" "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" diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml index 37db247497..81204afd58 100644 --- a/quickstep/res/values-sr/strings.xml +++ b/quickstep/res/values-sr/strings.xml @@ -22,6 +22,7 @@ "Закачи" "Слободни облик" "Рачунар" + "Рачунари" "Нема недавних ставки" "Подешавања коришћења апликације" "Обриши све" @@ -99,7 +100,7 @@ "Сачувај пар апликација" "Додирните другу апликацију за подељени екран" "Одаберите другу апликацију да бисте користили подељени екран" - "Откажи" + "Откажи" "Излазак из бирања подељеног екрана" "Одаберите другу апликацију за подељени екран" "Апликација или организација не дозвољавају ову радњу" @@ -130,18 +131,26 @@ "Брза подешавања" "Трака задатака" "Трака задатака је приказана" + "Приказ задатака/облачића лево" + "Приказ задатака/облачића десно" "Трака задатака је скривена" + "Скривени задаци/облачићи" "Трака за навигацију" "Увек приказуј траку задатака" "Промени режим навигације" "Разделник траке задатака" "Премести горе лево" "Премести доле десно" - "{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$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 f369daeb33..5d0c7a38fa 100644 --- a/quickstep/res/values-sv/strings.xml +++ b/quickstep/res/values-sv/strings.xml @@ -22,6 +22,7 @@ "Fäst" "Fritt format" "Dator" + "Dator" "Listan är tom" "Inställningar för appanvändning" "Rensa alla" @@ -99,7 +100,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 +131,26 @@ "Snabbinställn." "Aktivitetsfält" "Aktivitetsfältet visas" + "Vänster fält och bubblor visas" + "Höger fält och bubblor visas" "Aktivitetsfältet är dolt" + "Fält och bubblor dolda" "Navigeringsfält" "Visa alltid aktivitetsfältet" "Ändra navigeringsläge" "Avdelare 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.}}" + "{count,plural, =1{app till}other{appar till}}" + "Dator" "%1$s och %2$s" "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" diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml index 3d8277b448..e133ba30aa 100644 --- a/quickstep/res/values-sw/strings.xml +++ b/quickstep/res/values-sw/strings.xml @@ -22,6 +22,7 @@ "Bandika" "Muundo huru" "Kompyuta ya mezani" + "Kompyuta ya Mezani" "Hakuna vipengee vya hivi karibuni" "Mipangilio ya matumizi ya programu" "Ondoa zote" @@ -99,7 +100,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 +131,26 @@ "Mipangilio ya Haraka" "Upauzana" "Upauzana umeonyeshwa" + "Upauzana na viputo vinaonyeshwa kushoto" + "Upauzana na viputo vinaonyeshwa kulia" "Upauzana umefichwa" + "Upauzana na viputo vimefichwa" "Sehemu ya viungo muhimu" "Onyesha Zana kila wakati" "Badilisha hali ya usogezaji" "Kitenganishi cha Upauzana" "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.}}" + "{count,plural, =1{programu nyingine}other{programu zingine}}" + "Kompyuta ya Mezani" "%1$s na %2$s" "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" diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml index bde49a47f4..95d0fa9ab4 100644 --- a/quickstep/res/values-ta/strings.xml +++ b/quickstep/res/values-ta/strings.xml @@ -22,6 +22,7 @@ "பின் செய்தல்" "குறிப்பிட்ட வடிவமில்லாத பயன்முறை" "டெஸ்க்டாப்" + "டெஸ்க்டாப்" "சமீபத்தியவை எதுவுமில்லை" "ஆப்ஸ் உபயோக அமைப்புகள்" "எல்லாம் அழி" @@ -99,7 +100,7 @@ "ஆப்ஸ் ஜோடியைச் சேமி" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தேர்வுசெய்யுங்கள்" - "ரத்துசெய்" + "ரத்துசெய்" "திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்" "திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க" "ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை" @@ -130,18 +131,26 @@ "விரைவு அமைப்புகள்" "செயல் பட்டி" "செயல் பட்டி காட்டப்படுகிறது" + "செயல் பட்டி & குமிழை இடதுபுறம் காட்டும்" + "செயல் பட்டி & குமிழை வலதுபுறம் காட்டும்" "செயல் பட்டி மறைக்கப்பட்டுள்ளது" + "செயல் பட்டி & குமிழை மறைக்கும்" "வழிசெலுத்தல் பட்டி" "செயல் பட்டியை எப்போதும் காட்டு" "வழிசெலுத்தல் பயன்முறையை மாற்று" "செயல் பட்டிப் பிரிப்பான்" "மேலே/இடதுபுறம் நகர்த்தும்" "கீழே/வலதுபுறம் நகர்த்தும்" - "{count,plural, =1{மேலும் # ஆப்ஸைக் காட்டு.}other{மேலும் # ஆப்ஸைக் காட்டு.}}" - "{count,plural, =1{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}other{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}}" + "{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}" + "டெஸ்க்டாப்" "%1$s மற்றும் %2$s" "குமிழ்" "கூடுதல் விருப்பங்களைக் காட்டும்" "%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 a4e1cbf09c..116d388e43 100644 --- a/quickstep/res/values-te/strings.xml +++ b/quickstep/res/values-te/strings.xml @@ -22,6 +22,7 @@ "పిన్ చేయండి" "సంప్రదాయేతర" "డెస్క్‌టాప్" + "డెస్క్‌టాప్" "ఇటీవలి ఐటెమ్‌లు ఏవీ లేవు" "యాప్ వినియోగ సెట్టింగ్‌లు" "అన్నీ తీసివేయండి" @@ -99,7 +100,7 @@ "యాప్ పెయిర్‌ను సేవ్ చేయండి" "స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి" "స్ప్లిట్ స్క్రీన్‌ను ఉపయోగించడానికి మరొక యాప్ ఎంచుకోండి" - "రద్దు చేయండి" + "రద్దు చేయండి" "స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి" "స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి" "ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు" @@ -130,18 +131,26 @@ "క్విక్ సెట్టింగ్‌లు" "టాస్క్‌బార్" "టాస్క్‌బార్ చూపబడింది" + "టాస్క్‌బార్, బబుల్స్ ఎడమవైపున చూపబడ్డాయి" + "టాస్క్‌బార్, బబుల్స్ కుడివైపున చూపబడ్డాయి" "టాస్క్‌బార్ దాచబడింది" + "టాస్క్‌బార్, బబుల్స్ దాచబడినవి" "నావిగేషన్ బార్" "టాస్క్‌బార్‌ను నిరంతరం చూపండి" "నావిగేషన్ మోడ్‌ను మార్చండి" "టాస్క్‌బార్ డివైడర్" "ఎగువ/ఎడమ వైపునకు తరలించండి" "దిగువ/కుడి వైపునకు తరలించండి" - "{count,plural, =1{మరో # యాప్‌ను చూడండి.}other{మరో # యాప్‌లను చూడండి.}}" - "{count,plural, =1{# డెస్క్‌టాప్ యాప్‌ను చూపండి.}other{# డెస్క్‌టాప్ యాప్‌లను చూపండి.}}" + "{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}" + "డెస్క్‌టాప్" "%1$s, %2$s" "బబుల్" "ఓవర్‌ఫ్లో" "%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 3fcec684e4..ecdb3b5570 100644 --- a/quickstep/res/values-th/strings.xml +++ b/quickstep/res/values-th/strings.xml @@ -22,6 +22,7 @@ "ปักหมุด" "รูปแบบอิสระ" "เดสก์ท็อป" + "เดสก์ท็อป" "ไม่มีรายการล่าสุด" "การตั้งค่าการใช้แอป" "ล้างทั้งหมด" @@ -99,7 +100,7 @@ "บันทึกคู่แอป" "แตะแอปอื่นเพื่อใช้การแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" - "ยกเลิก" + "ยกเลิก" "ออกจากการเลือกโหมดแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" "แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้" @@ -130,18 +131,26 @@ "การตั้งค่าด่วน" "แถบงาน" "แถบงานแสดงอยู่" + "แถบงานและบับเบิลแสดงไว้ทางซ้าย" + "แถบงานและบับเบิลแสดงไว้ทางขวา" "แถบงานซ่อนอยู่" + "แถบงานและบับเบิลซ่อนอยู่" "แถบนำทาง" "แสดงแถบงานเสมอ" "เปลี่ยนโหมดการนําทาง" "ตัวแบ่งแถบงาน" "ย้ายไปที่ด้านบนหรือด้านซ้าย" "ย้ายไปที่ด้านล่างหรือด้านขวา" - "{count,plural, =1{แสดงเพิ่มเติมอีก # แอป}other{แสดงเพิ่มเติมอีก # แอป}}" - "{count,plural, =1{แสดงแอปบนเดสก์ท็อป # รายการ}other{แสดงแอปบนเดสก์ท็อป # รายการ}}" + "{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}" + "เดสก์ท็อป" "%1$s และ %2$s" "บับเบิล" "การดำเนินการเพิ่มเติม" "%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 978a5a37f8..da646a4d41 100644 --- a/quickstep/res/values-tl/strings.xml +++ b/quickstep/res/values-tl/strings.xml @@ -22,6 +22,7 @@ "I-pin" "Freeform" "Desktop" + "Desktop" "Walang kamakailang item" "Mga setting ng paggamit ng app" "I-clear lahat" @@ -99,7 +100,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 +131,26 @@ "Quick Settings" "Taskbar" "Ipinapakita ang taskbar" + "Taskbar at bubble sa kaliwa" + "Taskbar at bubble sa kanan" "Nakatago ang taskbar" + "Nakatago ang taskbar at bubble" "Navigation bar" "Ipakita lagi ang Taskbar" "Magpalit ng navigation mode" "Divider ng Taskbar" "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.}}" + "{count,plural, =1{pang app}one{pang app}other{pang app}}" + "Desktop" "%1$s at %2$s" "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" diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml index 7f84d85117..7be5464a19 100644 --- a/quickstep/res/values-tr/strings.xml +++ b/quickstep/res/values-tr/strings.xml @@ -22,6 +22,7 @@ "Sabitle" "Serbest çalışma" "Masaüstü" + "Masaüstü" "Yeni öğe yok" "Uygulama kullanım ayarları" "Tümünü temizle" @@ -99,7 +100,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 +131,26 @@ "Hızlı Ayarlar" "Görev çubuğu." "Görev çubuğu gösteriliyor" + "Görev çubuğu ve baloncuklar solda gösteriliyor" + "Görev çubuğu ve baloncuklar sağda gösteriliyor" "Görev çubuğu gizlendi" + "Görev çubuğu ve baloncuklar gizli" "Gezinme çubuğu" "Görev çubuğunu daima göster" "Gezinme modunu değiştir" "Görev Çubuğu Ayırıcısı" "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.}}" + "{count,plural, =1{uygulama daha}other{uygulama daha}}" + "Masaüstü" "%1$s ve %2$s" "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" diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml index d9274e5599..216ae0b700 100644 --- a/quickstep/res/values-uk/strings.xml +++ b/quickstep/res/values-uk/strings.xml @@ -22,6 +22,7 @@ "Закріпити" "Довільна форма" "Робочий стіл" + "Комп’ютер" "Немає нещодавніх додатків" "Налаштування використання додатка" "Очистити все" @@ -99,7 +100,7 @@ "Зберегти пару" "Щоб розділити екран, виберіть ще один додаток." "Щоб розділити екран, виберіть ще один додаток." - "Скасувати" + "Скасувати" "Вийти з режиму розділення екрана" "Щоб розділити екран, виберіть ще один додаток." "Ця дія заборонена додатком або адміністратором організації" @@ -130,18 +131,26 @@ "Швидкі налаштув." "Панель завдань" "Панель завдань показано" + "Панель завдань і чати – зліва" + "Панель завдань і чати – справа" "Панель завдань приховано" + "Панель і чати приховано" "Панель навігації" "Завжди показув. панель завдань" "Змінити режим навігації" "Розділювач панелі завдань" "Перемістити вгору або вліво" "Перемістити вниз або вправо" - "{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$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 e12524868f..2307cb12e1 100644 --- a/quickstep/res/values-ur/strings.xml +++ b/quickstep/res/values-ur/strings.xml @@ -22,6 +22,7 @@ "پن کریں" "فری فارم" "ڈیسک ٹاپ" + "ڈیسک ٹاپ" "کوئی حالیہ آئٹم نہیں" "ایپ کے استعمال کی ترتیبات" "سبھی کو صاف کریں" @@ -99,7 +100,7 @@ "ایپس کے جوڑے کو محفوظ کریں" "اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" - "منسوخ کریں" + "منسوخ کریں" "اسپلٹ اسکرین کے انتخاب سے باہر نکلیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" "ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے" @@ -130,18 +131,26 @@ "فوری ترتیبات" "ٹاسک بار" "ٹاشک بار دکھایا گیا" + "ٹاسک بار و بلبلے بائیں طرف ہیں" + "ٹاسک بار و بلبلے دائیں طرف ہیں" "ٹاسک بار چھپایا گیا" + "ٹاسک بار اور بلبلے پوشیدہ ہیں" "نیویگیشن بار" "ہمیشہ ٹاسک بار دکھائیں" "نیویگیشن موڈ تبدیل کریں" "ٹاسک بار ڈیوائیڈر" "اوپر/بائیں طرف منتقل کریں" "نیچے/دائیں طرف منتقل کریں" - "{count,plural, =1{# مزید ایپ دکھائیں۔}other{# مزید ایپس دکھائیں۔}}" - "{count,plural, =1{# ڈیسک ٹاپ ایپ دکھائیں۔}other{# ڈیسک ٹاپ ایپس دکھائیں۔}}" + "{count,plural, =1{مزید ایپ}other{مزید ایپس}}" + "ڈیسک ٹاپ" "%1$s اور %2$s" "ببل" "اوورفلو" "%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 3f4f981095..9f98b55452 100644 --- a/quickstep/res/values-uz/strings.xml +++ b/quickstep/res/values-uz/strings.xml @@ -22,6 +22,7 @@ "Qadash" "Erkin shakl" "Desktop" + "Desktop" "Yaqinda ishlatilgan ilovalar yo‘q" "Ilovadan foydalanish sozlamalari" "Hammasini tozalash" @@ -99,7 +100,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 +131,26 @@ "Tezkor sozlamalar" "Vazifalar paneli" "Vazifalar paneli ochiq" + "Panel va bulutchalar chapda" + "Panel va bulutchalar oʻngda" "Vazifalar paneli yopiq" + "Panel va bulutchalar berk" "Navigatsiya paneli" "Vazifalar paneli doim chiqarilsin" "Navigatsiya rejimini oʻzgartirish" "Vazifalar panelini ajratkich" "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.}}" + "{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}" + "Kompyuter" "%1$s va %2$s" "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" diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml index 9bc526faac..50acbbc7ac 100644 --- a/quickstep/res/values-vi/strings.xml +++ b/quickstep/res/values-vi/strings.xml @@ -22,6 +22,7 @@ "Ghim" "Dạng tự do" "Máy tính" + "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ả" @@ -99,7 +100,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 +131,26 @@ "Cài đặt nhanh" "Thanh tác vụ" "Đã hiệ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" "Đã ẩn thanh thao tác" + "Đã ẩn thanh tác vụ & bong bóng" "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" "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.}}" + "{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}" + "Máy tính" "%1$s%2$s" "Bong bóng" "Bong bóng bổ sung" "%1$s từ %2$s" "%1$s%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" diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml index f6e446e803..1b6c4e4a6f 100644 --- a/quickstep/res/values-zh-rCN/strings.xml +++ b/quickstep/res/values-zh-rCN/strings.xml @@ -22,6 +22,7 @@ "固定" "自由窗口" "桌面" + "桌面设备" "近期没有任何内容" "应用使用设置" "全部清除" @@ -99,7 +100,7 @@ "保存应用组合" "点按另一个应用即可使用分屏" "另外选择一个应用才可使用分屏模式" - "取消" + "取消" "退出分屏选择模式" "另外选择一个应用才可使用分屏模式" "该应用或您所在的单位不允许执行此操作" @@ -130,18 +131,26 @@ "快捷设置" "任务栏" "任务栏已显示" + "已显示任务栏和左侧消息气泡" + "已显示任务栏和右侧消息气泡" "任务栏已隐藏" + "已隐藏任务栏和消息气泡" "导航栏" "始终显示任务栏" "更改导航模式" "任务栏分隔线" "移到顶部/左侧" "移到底部/右侧" - "{count,plural, =1{显示另外 # 个应用。}other{显示另外 # 个应用。}}" - "{count,plural, =1{显示 # 款桌面应用。}other{显示 # 款桌面应用。}}" + "{count,plural, =1{多个应用}other{多个应用}}" + "桌面模式" "%1$s%2$s" "气泡框" "溢出式气泡框" "来自“%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 b9d8eb765a..38608e52b8 100644 --- a/quickstep/res/values-zh-rHK/strings.xml +++ b/quickstep/res/values-zh-rHK/strings.xml @@ -22,6 +22,7 @@ "固定" "自由形式" "桌面" + "桌面" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" @@ -99,7 +100,7 @@ "儲存應用程式組合" "輕按其他應用程式以使用分割螢幕" "選擇其他應用程式才能使用分割螢幕" - "取消" + "取消" "退出分割螢幕選取頁面" "選擇其他應用程式才能使用分割螢幕" "應用程式或你的機構不允許此操作" @@ -130,18 +131,26 @@ "快速設定" "工作列" "顯示咗工作列" + "工作列和對話氣泡在左邊顯示" + "工作列和對話氣泡在右邊顯示" "隱藏咗工作列" + "工作列和對話氣泡已隱藏" "導覽列" "一律顯示工作列" "變更導覽模式" "工作列分隔線" "移至上方/左側" "移至底部/右側" - "{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個桌面應用程式。}other{顯示 # 個桌面應用程式。}}" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "桌面" "「%1$s」和「%2$s」" "對話氣泡" "展開式" "%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 37e7d23a65..3e46779019 100644 --- a/quickstep/res/values-zh-rTW/strings.xml +++ b/quickstep/res/values-zh-rTW/strings.xml @@ -22,6 +22,7 @@ "固定" "自由形式" "桌面" + "電腦" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" @@ -99,7 +100,7 @@ "儲存應用程式配對" "輕觸另一個應用程式即可使用分割畫面" "選擇要在分割畫面中使用的另一個應用程式" - "取消" + "取消" "退出分割畫面選擇器" "必須選擇另一個應用程式才能使用分割畫面" "這個應用程式或貴機構不允許執行這個動作" @@ -130,18 +131,26 @@ "快速設定" "工作列" "已顯示工作列" + "工作列和對話框顯示在左側" + "工作列和對話框顯示在右側" "已隱藏工作列" + "已隱藏工作列和對話框" "導覽列" "一律顯示工作列" "變更操作模式" "工作列分隔線" "移到上方/左側" "移到底部/右側" - "{count,plural, =1{再多顯示 # 個應用程式。}other{再多顯示 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個電腦版應用程式。}other{顯示 # 個電腦版應用程式。}}" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "電腦" "「%1$s」和「%2$s」" "泡泡" "溢位" "「%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 73be445a55..120b38763d 100644 --- a/quickstep/res/values-zu/strings.xml +++ b/quickstep/res/values-zu/strings.xml @@ -22,6 +22,7 @@ "Phina" "I-Freeform" "Ideskithophu" + "Ideskithophu" "Azikho izinto zakamuva" "Izilungiselelo zokusetshenziswa kohlelo lokusebenza" "Sula konke" @@ -99,7 +100,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 +131,26 @@ "Amasethingi Asheshayo" "I-Taskbar" "Ibha yomsebenzi ibonisiwe" + "ITaskbar namabhamuza aboniswe kwesokunxele" + "ITaskbar namabhamuza aboniswe kwesokudla" "Ibha yomsebenzi ifihliwe" + "ITaskbar namabhamuza afihliwe" "Ibha yokufuna" "Bonisa i-Taskbar njalo." "Shintsha imodi yokufuna" "Isihlukanisi se-Taskbar" "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.}}" + "{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}" + "Ideskithophu" "I-%1$s ne-%2$s" "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" diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml index ccc7f180dd..7fd6b5c037 100644 --- a/quickstep/res/values/attrs.xml +++ b/quickstep/res/values/attrs.xml @@ -28,6 +28,7 @@ + diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml index 14a916f68b..4c48bd3f99 100644 --- a/quickstep/res/values/colors.xml +++ b/quickstep/res/values/colors.xml @@ -76,7 +76,7 @@ #80868b #bdc1c6 - #FFFFFFFF + @android:color/system_neutral1_50 #333333 @@ -94,7 +94,7 @@ #f9ab00 - ?androidprv:attr/colorAccentPrimaryVariant - ?androidprv:attr/materialColorPrimaryFixedDim - ?androidprv:attr/materialColorOnPrimaryFixed + ?attr/materialColorPrimary + ?attr/materialColorPrimaryFixedDim + ?attr/materialColorOnPrimaryFixed \ No newline at end of file diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index fd122103e1..e8cb5d5241 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -35,7 +35,7 @@ com.android.quickstep.LauncherRestoreEventLoggerImpl com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl com.android.launcher3.taskbar.TaskbarEduTooltipController - + com.android.quickstep.contextualeducation.SystemContextualEduStatsManager diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 5c82c991c7..8957e0d16e 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -41,6 +41,8 @@ 48dp 44dp + + 52dp 4dp @@ -348,7 +350,6 @@ 48dp 48dp 32dp - 6dp 6dp 64dp 64dp @@ -374,6 +375,7 @@ 400dp 8dp 16dp + 4dp 16dp @@ -434,6 +436,7 @@ 55dp @dimen/transient_taskbar_stashed_height @dimen/taskbar_stashed_handle_height + @dimen/transient_taskbar_stash_spring_velocity_dp_per_s 9dp @@ -445,14 +448,17 @@ 1dp 2dp 90dp + 20dp 32dp 36dp + 28dp 24dp 12dp 16dp 6dp 8dp + @dimen/bubblebar_icon_spacing 12dp 1dp @@ -473,6 +479,17 @@ 24dp 16dp + + 16dp + 4dp + 14dp + 238dp + 276dp + 12dp + 10dp + 1dp + 2dp + @@ -481,17 +498,22 @@ 4dp 104dp - 134dp + 136dp 52dp 20dp + 32dp 56dp 16dp 16dp - 2dp + 4dp 28dp 16dp 24dp 8dp + 104dp + 360dp + 16dp + 20dp 48dp diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/values/ids.xml similarity index 69% rename from quickstep/res/drawable/bg_circle.xml rename to quickstep/res/values/ids.xml index 506177b6a1..3091d9e930 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 340d25b0f9..008766b51b 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -26,9 +26,12 @@ Pin Freeform - + Desktop + + Desktop + No recent items @@ -237,7 +240,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 +302,16 @@ Quick Settings Taskbar - + Taskbar shown - + + Taskbar & bubbles left shown + + Taskbar & bubbles right shown + Taskbar hidden + + Taskbar & bubbles hidden Navigation bar @@ -311,6 +320,8 @@ Change navigation mode Taskbar Divider + + Taskbar Overflow @@ -318,17 +329,14 @@ Move to bottom/right - + {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 @@ -342,4 +350,14 @@ %1$s from %2$s %1$s and %2$d more + + Move left + + Move right + + Dismiss all + + expand %1$s + + collapse %1$s diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index 952505a484..c423d094bf 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -124,7 +124,7 @@ - + + + + - diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index 15180efbcc..a63ba0f8e6 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -26,7 +26,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.content.Context; import android.os.Handler; -import android.os.RemoteException; +import android.util.Log; import android.view.IRemoteAnimationFinishedCallback; import android.view.RemoteAnimationTarget; @@ -196,6 +196,7 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { if (skipFirstFrame) { // Because t=0 has the app icon in its original spot, we can skip the // first frame and have the same movement one frame earlier. + Log.d("b/311077782", "LauncherAnimationRunner.setAnimation"); mAnimator.setCurrentPlayTime( Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration())); } @@ -208,7 +209,7 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { * animation finished runnable. */ @Override - public void onAnimationFinished() throws RemoteException { + public void onAnimationFinished() { mASyncFinishRunnable.run(); } } diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java index 962fd91c2e..1161720e3b 100644 --- a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java +++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java @@ -15,10 +15,19 @@ */ package com.android.launcher3; +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + import android.view.KeyEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.allapps.AllAppsRecyclerView; +import com.android.launcher3.allapps.SearchRecyclerView; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -26,13 +35,60 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import java.util.List; public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate { + private QuickstepLauncher mLauncher; public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) { super(launcher); + mLauncher = launcher; mActions.put(PIN_PREDICTION, new LauncherAction( PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P)); } + @Override + public void onPopulateAccessibilityEvent(View view, AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(view, event); + // Scroll to the position if focused view in main allapps list and not completely visible. + scrollToPositionIfNeeded(view); + } + + private void scrollToPositionIfNeeded(View view) { + if (!Flags.accessibilityScrollOnAllapps()) { + return; + } + AllAppsRecyclerView contentView = mLauncher.getAppsView().getActiveRecyclerView(); + if (contentView instanceof SearchRecyclerView) { + return; + } + LinearLayoutManager layoutManager = (LinearLayoutManager) contentView.getLayoutManager(); + if (layoutManager == null) { + return; + } + RecyclerView.ViewHolder vh = contentView.findContainingViewHolder(view); + if (vh == null) { + return; + } + int itemPosition = vh.getBindingAdapterPosition(); + if (itemPosition == NO_POSITION) { + return; + } + int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition(); + int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition(); + boolean itemCompletelyVisible = firstCompletelyVisible <= itemPosition + && lastCompletelyVisible >= itemPosition; + if (itemCompletelyVisible) { + return; + } + RecyclerView.SmoothScroller smoothScroller = + new LinearSmoothScroller(mLauncher.asContext()) { + @Override + protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_ANY; + } + }; + smoothScroller.setTargetPosition(itemPosition); + layoutManager.startSmoothScroll(smoothScroller); + } + @Override protected void getSupportedActions(View host, ItemInfo item, List out) { if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) { diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index fae281a70a..a64936d287 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -43,19 +43,15 @@ import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS; import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION; +import static com.android.launcher3.Flags.enableContainerReturnAnimations; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; -import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.Utilities.mapBoundToRange; -import static com.android.launcher3.config.FeatureFlags.ENABLE_BACK_SWIPE_HOME_ANIMATION; -import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH; -import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION; import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY; -import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; import static com.android.launcher3.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE; import static com.android.launcher3.util.DisplayController.isTransientTaskbar; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; @@ -64,10 +60,10 @@ import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VAL import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; import static com.android.launcher3.views.FloatingIconView.getFloatingIconView; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch; import static com.android.quickstep.util.AnimUtils.clampToDuration; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; @@ -88,7 +84,6 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.IBinder; @@ -115,6 +110,7 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.window.RemoteTransition; import android.window.TransitionFilter; +import android.window.WindowAnimationState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -137,16 +133,17 @@ import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.DynamicResource; -import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.RunnableList; -import com.android.launcher3.util.Themes; +import com.android.launcher3.util.StableViewInfo; import com.android.launcher3.views.FloatingIconView; -import com.android.launcher3.views.ScrimView; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.quickstep.LauncherBackAnimationController; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskViewUtils; +import com.android.quickstep.util.AlreadyStartedBackAnimState; +import com.android.quickstep.util.AnimatorBackState; +import com.android.quickstep.util.BackAnimState; import com.android.quickstep.util.MultiValueUpdateListener; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig; @@ -173,6 +170,7 @@ import com.android.wm.shell.startingsurface.IStartingWindowListener; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -254,7 +252,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Strong refs to runners which are cleared when the launcher activity is destroyed private RemoteAnimationFactory mWallpaperOpenRunner; private RemoteAnimationFactory mAppLaunchRunner; - private RemoteAnimationFactory mKeyguardGoingAwayRunner; private RemoteAnimationFactory mWallpaperOpenTransitionRunner; private RemoteTransition mLauncherOpenTransition; @@ -323,7 +320,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @return ActivityOptions with remote animations that controls how the window of the opening * targets are displayed. */ - public ActivityOptionsWrapper getActivityLaunchOptions(View v) { + public ActivityOptionsWrapper getActivityLaunchOptions(View v, ItemInfo itemInfo) { boolean fromRecents = isLaunchingFromRecents(v, null /* targets */); RunnableList onEndCallback = new RunnableList(); @@ -333,17 +330,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener restartedListener.register(onEndCallback::executeAllAndDestroy); onEndCallback.add(restartedListener::unregister); - mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback); - ItemInfo tag = (ItemInfo) v.getTag(); - if (tag != null && tag.shouldUseBackgroundAnimation()) { - ContainerAnimationRunner containerAnimationRunner = ContainerAnimationRunner.from( - v, mLauncher, mStartingWindowListener, onEndCallback); - if (containerAnimationRunner != null) { - mAppLaunchRunner = containerAnimationRunner; - } - } - RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner( - mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback); // Note that this duration is a guess as we do not know if the animation will be a // recents launch or not for sure until we know the opening app targets. @@ -360,9 +347,30 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback); options.setOnAnimationAbortListener(endCallback); options.setOnAnimationFinishedListener(endCallback); + options.setLaunchCookie(StableViewInfo.toLaunchCookie(itemInfo)); return new ActivityOptionsWrapper(options, onEndCallback); } + /** + * Selects the appropriate type of launch runner for the given view, builds it, and returns it. + * {@link QuickstepTransitionManager#mAppLaunchRunner} is updated as a by-product of this + * method. + */ + private RemoteAnimationRunnerCompat createAppLaunchRunner(View v, RunnableList onEndCallback) { + ItemInfo tag = (ItemInfo) v.getTag(); + ContainerAnimationRunner containerRunner = null; + if (tag != null && tag.shouldUseBackgroundAnimation()) { + containerRunner = ContainerAnimationRunner.fromView( + v, true /* forLaunch */, mLauncher, mStartingWindowListener, onEndCallback, + null /* windowState */); + } + + mAppLaunchRunner = containerRunner != null + ? containerRunner : new AppLaunchAnimationRunner(v, onEndCallback); + return new LauncherAnimationRunner( + mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + } + /** * Whether the launch is a recents app transition and we should do a launch animation * from the recents view. Note that if the remote animation targets are not provided, this @@ -588,34 +596,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener launcherAnimator.play(scaleAnim); }); - final boolean scrimEnabled = ENABLE_SCRIM_FOR_APP_LAUNCH.get(); - if (scrimEnabled) { - int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor); - int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0); - int[] colors = isAppOpening - ? new int[]{scrimColorTrans, scrimColor} - : new int[]{scrimColor, scrimColorTrans}; - ScrimView scrimView = mLauncher.getScrimView(); - if (scrimView.getBackground() instanceof ColorDrawable) { - scrimView.setBackgroundColor(colors[0]); - - ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR, - colors); - scrim.setDuration(CONTENT_SCRIM_DURATION); - scrim.setInterpolator(DECELERATE_1_5); - - launcherAnimator.play(scrim); - } - } - endListener = () -> { viewsToAnimate.forEach(view -> { SCALE_PROPERTY.set(view, 1f); view.setLayerType(View.LAYER_TYPE_NONE, null); }); - if (scrimEnabled) { - mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT); - } mLauncher.resumeExpensiveViewUpdates(); }; } @@ -1124,38 +1109,25 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * additional animations. */ private void addRemoteAnimations(RemoteAnimationDefinition definition) { - mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */); + mWallpaperOpenRunner = new WallpaperOpenLauncherAnimationRunner(); definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN, WindowConfiguration.ACTIVITY_TYPE_STANDARD, new RemoteAnimationAdapter( new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner, false /* startAtFrontOfQueue */), CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); - - if (KEYGUARD_ANIMATION.get()) { - mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */); - definition.addRemoteAnimation( - WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER, - new RemoteAnimationAdapter( - new LauncherAnimationRunner( - mHandler, mKeyguardGoingAwayRunner, - true /* startAtFrontOfQueue */), - CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); - } } /** * Registers remote animations used when closing apps to home screen. */ public void registerRemoteTransitions() { - if (ENABLE_SHELL_TRANSITIONS) { - SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue(); - } + SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue(); if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } - mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */); + mWallpaperOpenTransitionRunner = new WallpaperOpenLauncherAnimationRunner(); mLauncherOpenTransition = new RemoteTransition( new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner, false /* startAtFrontOfQueue */).toRemoteTransition(), @@ -1164,15 +1136,25 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener TransitionFilter homeCheck = new TransitionFilter(); // No need to handle the transition that also dismisses keyguard. homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + homeCheck.mRequirements = new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(), + new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; + homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME; homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName(); homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP; + homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD; homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + + homeCheck.mRequirements[2].mNot = true; + homeCheck.mRequirements[2].mCustomAnimation = true; + homeCheck.mRequirements[2].mMustBeTask = true; + homeCheck.mRequirements[2].mMustBeIndependent = true; + SystemUiProxy.INSTANCE.get(mLauncher) .registerRemoteTransition(mLauncherOpenTransition, homeCheck); if (mBackAnimationController != null) { @@ -1200,13 +1182,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // definition so we don't have to wait for the system gc mWallpaperOpenRunner = null; mAppLaunchRunner = null; - mKeyguardGoingAwayRunner = null; } protected void unregisterRemoteTransitions() { - if (ENABLE_SHELL_TRANSITIONS) { - SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue(); - } + SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue(); if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } @@ -1260,41 +1239,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return false; } - /** - * @return Runner that plays when user goes to Launcher - * ie. pressing home, swiping up from nav bar. - */ - RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) { - return new WallpaperOpenLauncherAnimationRunner(fromUnlock); - } - - /** - * Animator that controls the transformations of the windows when unlocking the device. - */ - private Animator getUnlockWindowAnimator(RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets) { - SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer); - ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1); - unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS); - float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 : - QuickStepContract.getWindowCornerRadius(mLauncher); - unlockAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - SurfaceTransaction transaction = new SurfaceTransaction(); - for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTarget target = appTargets[i]; - transaction.forSurface(target.leash) - .setAlpha(1f) - .setWindowCrop(target.screenSpaceBounds) - .setCornerRadius(cornerRadius); - } - surfaceApplier.scheduleApply(transaction); - } - }); - return unlockAnimator; - } - private static int getRotationChange(RemoteAnimationTarget[] appTargets) { int rotationChange = 0; for (RemoteAnimationTarget target : appTargets) { @@ -1348,20 +1292,12 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Find the associated item info for the launch cookie (if available), note that predicted // apps actually have an id of -1, so use another default id here - final ArrayList launchCookies = runningTaskTarget.taskInfo.launchCookies == null - ? new ArrayList<>() + final List launchCookies = runningTaskTarget.taskInfo.launchCookies == null + ? Collections.EMPTY_LIST : runningTaskTarget.taskInfo.launchCookies; - int launchCookieItemId = NO_MATCHING_ID; - for (IBinder cookie : launchCookies) { - Integer itemId = ObjectWrapper.unwrap(cookie); - if (itemId != null) { - launchCookieItemId = itemId; - break; - } - } - - return mLauncher.getFirstMatchForAppClose(launchCookieItemId, packageName, + return mLauncher.getFirstMatchForAppClose( + StableViewInfo.fromLaunchCookies(launchCookies), packageName, UserHandle.of(runningTaskTarget.taskInfo.userId), true /* supportsAllAppsState */); } @@ -1564,7 +1500,15 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } private void addCujInstrumentation(Animator anim, int cuj) { - anim.addListener(new AnimationSuccessListener() { + anim.addListener(getCujAnimationSuccessListener(cuj)); + } + + private void addCujInstrumentation(RectFSpringAnim anim, int cuj) { + anim.addAnimatorListener(getCujAnimationSuccessListener(cuj)); + } + + private AnimationSuccessListener getCujAnimationSuccessListener(int cuj) { + return new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { mDragLayer.getViewTreeObserver().addOnDrawListener( @@ -1598,27 +1542,55 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public void onAnimationSuccess(Animator animator) { InteractionJankMonitorWrapper.end(cuj); } - }); + }; } /** * Creates the {@link RectFSpringAnim} and {@link AnimatorSet} required to animate * the transition. */ - public Pair createWallpaperOpenAnimations( + @NonNull + public BackAnimState createWallpaperOpenAnimations( RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - boolean fromUnlock, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonAppTargets, RectF startRect, float startWindowCornerRadius, boolean fromPredictiveBack) { + View launcherView = findLauncherView(appTargets); + if (checkReturnAnimationsFlags() + && launcherView != null + && launcherView.getTag() instanceof ItemInfo info + && info.shouldUseBackgroundAnimation()) { + // Try to create a return animation + RunnableList onEndCallback = new RunnableList(); + WindowAnimationState windowState = new WindowAnimationState(); + windowState.bounds = startRect; + windowState.bottomLeftRadius = windowState.bottomRightRadius = + windowState.topLeftRadius = windowState.topRightRadius = + startWindowCornerRadius; + ContainerAnimationRunner runner = ContainerAnimationRunner.fromView( + launcherView, false /* forLaunch */, mLauncher, mStartingWindowListener, + onEndCallback, windowState); + if (runner != null) { + runner.startAnimation(TRANSIT_CLOSE, + appTargets, wallpapers, nonAppTargets, + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() { + onEndCallback.executeAllAndDestroy(); + } + }); + return new AlreadyStartedBackAnimState(onEndCallback); + } + } + AnimatorSet anim = new AnimatorSet(); RectFSpringAnim rectFSpringAnim = null; final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible() || launcherIsATargetWithMode(appTargets, MODE_OPENING); - View launcherView = findLauncherView(appTargets); boolean playFallBackAnimation = (launcherView == null && launcherIsForceInvisibleOrOpening) || mLauncher.getWorkspace().isOverlayShown() @@ -1626,10 +1598,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener boolean playWorkspaceReveal = !fromPredictiveBack; boolean skipAllAppsScale = false; - if (fromUnlock) { - anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets)); - } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get() - && !playFallBackAnimation) { + if (!playFallBackAnimation) { PointF velocity; if (enableScalingRevealHomeAnimation()) { velocity = new PointF(); @@ -1679,10 +1648,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // invisibility on touch down, and only reset it after the animation to home // is initialized. if (launcherIsForceInvisibleOrOpening || fromPredictiveBack) { - addCujInstrumentation(anim, playFallBackAnimation - ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK - : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); - AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -1692,6 +1657,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }; + if (rectFSpringAnim != null && anim.getChildAnimations().isEmpty()) { + addCujInstrumentation(rectFSpringAnim, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); + } else { + addCujInstrumentation(anim, playFallBackAnimation + ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK + : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); + } + if (fromPredictiveBack && rectFSpringAnim != null) { rectFSpringAnim.addAnimatorListener(endListener); } else { @@ -1717,7 +1690,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - return new Pair(rectFSpringAnim, anim); + return new AnimatorBackState(rectFSpringAnim, anim); } public static int getTaskbarToHomeDuration() { @@ -1728,17 +1701,15 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } + private static boolean checkReturnAnimationsFlags() { + return enableContainerReturnAnimations() && returnAnimationFrameworkLibrary(); + } + /** * Remote animation runner for animation from the app to Launcher, including recents. */ protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory { - private final boolean mFromUnlock; - - public WallpaperOpenLauncherAnimationRunner(boolean fromUnlock) { - mFromUnlock = fromUnlock; - } - @Override public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets, @@ -1769,14 +1740,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - Pair pair = createWallpaperOpenAnimations( - appTargets, wallpaperTargets, mFromUnlock, resolveRectF, + BackAnimState bankAnimState = createWallpaperOpenAnimations( + appTargets, wallpaperTargets, nonAppTargets, resolveRectF, QuickStepContract.getWindowCornerRadius(mLauncher), false /* fromPredictiveBack */); TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, false, null); mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); - result.setAnimation(pair.second, mLauncher); + bankAnimState.applyToAnimationResult(result, mLauncher); } } @@ -1850,32 +1821,29 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } @Nullable - private static ContainerAnimationRunner from(View v, Launcher launcher, - StartingWindowListener startingWindowListener, RunnableList onEndCallback) { - View viewToUse = findLaunchableViewWithBackground(v); - if (viewToUse == null) { - return null; + static ContainerAnimationRunner fromView( + View v, + boolean forLaunch, + Launcher launcher, + StartingWindowListener startingWindowListener, + RunnableList onEndCallback, + @Nullable WindowAnimationState windowState) { + if (!forLaunch && !checkReturnAnimationsFlags()) { + throw new IllegalStateException( + "forLaunch cannot be false when the enableContainerReturnAnimations or " + + "returnAnimationFrameworkLibrary flag is disabled"); } - // The CUJ is logged by the click handler, so we don't log it inside the animation - // library. - ActivityTransitionAnimator.Controller controllerDelegate = - ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); - - if (controllerDelegate == null) { - return null; - } - - // This wrapper allows us to override the default value, telling the controller that the - // current window is below the animating window. + // First the controller is created. This is used by the runner to animate the + // origin/target view. ActivityTransitionAnimator.Controller controller = - new DelegateTransitionAnimatorController(controllerDelegate) { - @Override - public boolean isBelowAnimatingWindow() { - return true; - } - }; + buildController(v, forLaunch, windowState); + if (controller == null) { + return null; + } + // The callback is used to make sure that we use the right color to fade between view + // and the window. ActivityTransitionAnimator.Callback callback = task -> { final int backgroundColor = startingWindowListener.mBackgroundColor == Color.TRANSPARENT @@ -1897,6 +1865,50 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener MAIN_EXECUTOR, controller, callback, listener)); } + /** + * Constructs a {@link ActivityTransitionAnimator.Controller} that can be used by a + * {@link ContainerAnimationRunner} to animate a view into an opening window or from a + * closing one. + */ + @Nullable + private static ActivityTransitionAnimator.Controller buildController( + View v, boolean isLaunching, @Nullable WindowAnimationState windowState) { + View viewToUse = findLaunchableViewWithBackground(v); + if (viewToUse == null) { + return null; + } + + // The CUJ is logged by the click handler, so we don't log it inside the animation + // library. TODO: figure out return CUJ. + ActivityTransitionAnimator.Controller controllerDelegate = + ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); + + if (controllerDelegate == null) { + return null; + } + + // This wrapper allows us to override the default value, telling the controller that the + // current window is below the animating window as well as information about the return + // animation. + return new DelegateTransitionAnimatorController(controllerDelegate) { + @Override + public boolean isLaunching() { + return isLaunching; + } + + @Override + public boolean isBelowAnimatingWindow() { + return true; + } + + @Nullable + @Override + public WindowAnimationState getWindowAnimatorState() { + return windowState; + } + }; + } + /** * Finds the closest parent of [view] (inclusive) that implements {@link LaunchableView} and * has a background drawable. @@ -1906,13 +1918,12 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener View view) { View current = view; while (current.getBackground() == null || !(current instanceof LaunchableView)) { - if (!(current.getParent() instanceof View)) { + if (current.getParent() instanceof View v) { + current = v; + } else { return null; } - - current = (View) current.getParent(); } - return (T) current; } @@ -1920,6 +1931,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets, RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, LauncherAnimationRunner.AnimationResult result) { + startAnimation( + transit, appTargets, wallpaperTargets, nonAppTargets, result); + } + + public void startAnimation(int transit, RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, + IRemoteAnimationFinishedCallback result) { mDelegate.onAnimationStart( transit, appTargets, wallpaperTargets, nonAppTargets, result); } diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java index 943c08c786..955388dec7 100644 --- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java +++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java @@ -33,29 +33,34 @@ import android.util.Log; import android.view.View; import android.view.WindowInsetsController; import android.view.WindowManager; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.dragndrop.SimpleDragLayer; +import com.android.launcher3.model.StringCache; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.WidgetPredictionsRequester; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.popup.PopupDataProvider; -import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.widget.BaseWidgetSheet; +import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder; import com.android.launcher3.widget.model.WidgetsListBaseEntry; -import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** An Activity that can host Launcher's widget picker. */ public class WidgetPickerActivity extends BaseActivity { @@ -77,6 +82,15 @@ public class WidgetPickerActivity extends BaseActivity { *

This allows widget picker to exclude existing widgets from suggestions.

*/ private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; + /** + * Intent extra for the string representing the title displayed within the picker header. + */ + private static final String EXTRA_PICKER_TITLE = "picker_title"; + /** + * Intent extra for the string representing the description displayed within the picker header. + */ + private static final String EXTRA_PICKER_DESCRIPTION = "picker_description"; + /** * A unique identifier of the surface hosting the widgets; *

"widgets" is reserved for home screen surface.

@@ -85,11 +99,19 @@ public class WidgetPickerActivity extends BaseActivity { private static final String EXTRA_UI_SURFACE = "ui_surface"; private static final Pattern UI_SURFACE_PATTERN = Pattern.compile("^(widgets|widgets_hub)$"); + + /** + * User ids that should be filtered out of the widget lists created by this activity. + */ + private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids"; + private SimpleDragLayer mDragLayer; private WidgetsModel mModel; private LauncherAppState mApp; + private StringCache mStringCache; private WidgetPredictionsRequester mWidgetPredictionsRequester; - private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {}); + private final WidgetPickerDataProvider mWidgetPickerDataProvider = + new WidgetPickerDataProvider(); private int mDesiredWidgetWidth; private int mDesiredWidgetHeight; @@ -99,6 +121,30 @@ public class WidgetPickerActivity extends BaseActivity { // Widgets existing on the host surface. @NonNull private List mAddedWidgets = new ArrayList<>(); + @Nullable + private String mTitle; + @Nullable + private String mDescription; + + /** A set of user ids that should be filtered out from the selected widgets. */ + @NonNull + Set mFilteredUserIds = new HashSet<>(); + + @Nullable + private WidgetsFullSheet mWidgetSheet; + + private final Predicate mWidgetsFilter = widget -> { + final WidgetAcceptabilityVerdict verdict = + isWidgetAcceptable(widget, /* applySizeFilter=*/ false); + verdict.maybeLogVerdict(); + return verdict.isAcceptable; + }; + private final Predicate mDefaultWidgetsFilter = widget -> { + final WidgetAcceptabilityVerdict verdict = + isWidgetAcceptable(widget, /* applySizeFilter=*/ true); + verdict.maybeLogVerdict(); + return verdict.isAcceptable; + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -119,15 +165,21 @@ public class WidgetPickerActivity extends BaseActivity { WindowInsetsController wc = mDragLayer.getWindowInsetsController(); wc.hide(navigationBars() + statusBars()); - BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true); - widgetSheet.disableNavBarScrim(true); - widgetSheet.addOnCloseListener(this::finish); - parseIntentExtras(); refreshAndBindWidgets(); } + @Override + protected void registerBackDispatcher() { + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + new BackAnimationCallback()); + } + private void parseIntentExtras() { + mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE); + mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION); + // A value of 0 for either size means that no filtering will occur in that dimension. If // both values are 0, then no size filtering will occur. mDesiredWidgetWidth = @@ -148,12 +200,18 @@ public class WidgetPickerActivity extends BaseActivity { if (addedWidgets != null) { mAddedWidgets = addedWidgets; } + ArrayList filteredUsers = getIntent().getIntegerArrayListExtra( + EXTRA_USER_ID_FILTER); + mFilteredUserIds.clear(); + if (filteredUsers != null) { + mFilteredUserIds.addAll(filteredUsers); + } } @NonNull @Override - public PopupDataProvider getPopupDataProvider() { - return mPopupDataProvider; + public WidgetPickerDataProvider getWidgetPickerDataProvider() { + return mWidgetPickerDataProvider; } @Override @@ -223,42 +281,62 @@ public class WidgetPickerActivity extends BaseActivity { }; } - /** Updates the model with widgets and provides them after applying the provided filter. */ + /** + * Updates the model with widgets, applies filters and launches the widgets sheet once + * widgets are available + */ private void refreshAndBindWidgets() { MODEL_EXECUTOR.execute(() -> { LauncherAppState app = LauncherAppState.getInstance(this); mModel.update(app, null); - final List allWidgets = - mModel.getFilteredWidgetsListForPicker( - app.getContext(), - /*widgetItemFilter=*/ widget -> { - final WidgetAcceptabilityVerdict verdict = - isWidgetAcceptable(widget); - verdict.maybeLogVerdict(); - return verdict.isAcceptable; - } - ); - bindWidgets(allWidgets); + + StringCache stringCache = new StringCache(); + stringCache.loadStrings(this); + + bindStringCache(stringCache); + bindWidgets(mModel.getWidgetsByPackageItem()); + // Open sheet once widgets are available, so that it doesn't interrupt the open + // animation. + openWidgetsSheet(); if (mUiSurface != null) { - Map> allWidgetsMap = allWidgets.stream() - .filter(WidgetsListHeaderEntry.class::isInstance) - .collect(Collectors.toMap( - entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem), - entry -> entry.mWidgets) - ); mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), - mUiSurface, allWidgetsMap); + mUiSurface, mModel.getWidgetsByComponentKey()); mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); } }); } - private void bindWidgets(List widgets) { - MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); + private void bindStringCache(final StringCache stringCache) { + MAIN_EXECUTOR.execute(() -> mStringCache = stringCache); + } + + private void bindWidgets(Map> widgets) { + WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder( + mApp.getContext()); + + final List allWidgets = builder.build(widgets, mWidgetsFilter); + final List defaultWidgets = + shouldShowDefaultWidgets() ? builder.build(widgets, + mDefaultWidgetsFilter) : List.of(); + + MAIN_EXECUTOR.execute( + () -> mWidgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets)); + } + + private void openWidgetsSheet() { + MAIN_EXECUTOR.execute(() -> { + mWidgetSheet = WidgetsFullSheet.show(this, true); + mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription); + mWidgetSheet.disableNavBarScrim(true); + mWidgetSheet.addOnCloseListener(this::finish); + }); } private void bindRecommendedWidgets(List recommendedWidgets) { - MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); + // Bind recommendations once picker has finished open animation. + MAIN_EXECUTOR.getHandler().postDelayed( + () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets), + mDeviceProfile.bottomSheetOpenDuration); } @Override @@ -269,12 +347,76 @@ public class WidgetPickerActivity extends BaseActivity { } } - private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) { + @Nullable + @Override + public StringCache getStringCache() { + return mStringCache; + } + + /** + * Animation callback for different predictive back animation states for the widget picker. + */ + private class BackAnimationCallback implements OnBackAnimationCallback { + @Nullable + OnBackAnimationCallback mActiveOnBackAnimationCallback; + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + if (mActiveOnBackAnimationCallback != null) { + mActiveOnBackAnimationCallback.onBackCancelled(); + } + if (mWidgetSheet != null) { + mActiveOnBackAnimationCallback = mWidgetSheet; + mActiveOnBackAnimationCallback.onBackStarted(backEvent); + } + } + + @Override + public void onBackInvoked() { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackInvoked(); + mActiveOnBackAnimationCallback = null; + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackProgressed(backEvent); + } + + @Override + public void onBackCancelled() { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackCancelled(); + mActiveOnBackAnimationCallback = null; + } + } + + private boolean shouldShowDefaultWidgets() { + // If optional filters such as size filter are present, we display them as default widgets. + return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0; + } + + private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget, + boolean applySizeFilter) { final AppWidgetProviderInfo info = widget.widgetInfo; if (info == null) { return rejectWidget(widget, "shortcut"); } + if (mFilteredUserIds.contains(widget.user.getIdentifier())) { + return rejectWidget( + widget, + "widget user: %d is being filtered", + widget.user.getIdentifier()); + } + if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) { return rejectWidget( widget, @@ -283,61 +425,63 @@ public class WidgetPickerActivity extends BaseActivity { info.widgetCategory); } - if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) { - // Accept the widget if the desired dimensions are unspecified. - return acceptWidget(widget); - } - - final boolean isHorizontallyResizable = - (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; - if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) { - if (info.maxResizeWidth > 0 - && info.maxResizeWidth >= info.minWidth - && info.maxResizeWidth < mDesiredWidgetWidth) { - return rejectWidget( - widget, - "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]", - info.maxResizeWidth, - mDesiredWidgetWidth); + if (applySizeFilter) { + if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) { + // Accept the widget if the desired dimensions are unspecified. + return acceptWidget(widget); } - final int minWidth = Math.min(info.minResizeWidth, info.minWidth); - if (minWidth > mDesiredWidgetWidth) { - return rejectWidget( - widget, - "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]", - info.minWidth, - info.minResizeWidth, - mDesiredWidgetWidth); - } - } + final boolean isHorizontallyResizable = + (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; + if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) { + if (info.maxResizeWidth > 0 + && info.maxResizeWidth >= info.minWidth + && info.maxResizeWidth < mDesiredWidgetWidth) { + return rejectWidget( + widget, + "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]", + info.maxResizeWidth, + mDesiredWidgetWidth); + } - final boolean isVerticallyResizable = - (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; - if (mDesiredWidgetHeight > 0 && isVerticallyResizable) { - if (info.maxResizeHeight > 0 - && info.maxResizeHeight >= info.minHeight - && info.maxResizeHeight < mDesiredWidgetHeight) { - return rejectWidget( - widget, - "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]", - info.maxResizeHeight, - mDesiredWidgetHeight); + final int minWidth = Math.min(info.minResizeWidth, info.minWidth); + if (minWidth > mDesiredWidgetWidth) { + return rejectWidget( + widget, + "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]", + info.minWidth, + info.minResizeWidth, + mDesiredWidgetWidth); + } } - final int minHeight = Math.min(info.minResizeHeight, info.minHeight); - if (minHeight > mDesiredWidgetHeight) { - return rejectWidget( - widget, - "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]", - info.minHeight, - info.minResizeHeight, - mDesiredWidgetHeight); - } - } + final boolean isVerticallyResizable = + (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; + if (mDesiredWidgetHeight > 0 && isVerticallyResizable) { + if (info.maxResizeHeight > 0 + && info.maxResizeHeight >= info.minHeight + && info.maxResizeHeight < mDesiredWidgetHeight) { + return rejectWidget( + widget, + "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]", + info.maxResizeHeight, + mDesiredWidgetHeight); + } - if (!isHorizontallyResizable || !isVerticallyResizable) { - return rejectWidget(widget, "not resizeable"); + final int minHeight = Math.min(info.minResizeHeight, info.minHeight); + if (minHeight > mDesiredWidgetHeight) { + return rejectWidget( + widget, + "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]", + info.minHeight, + info.minResizeHeight, + mDesiredWidgetHeight); + } + } + + if (!isHorizontallyResizable || !isVerticallyResizable) { + return rejectWidget(widget, "not resizeable"); + } } return acceptWidget(widget); diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java index 84c2ed252b..7a8b58e58f 100644 --- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java +++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java @@ -31,12 +31,12 @@ import android.view.View; import android.view.accessibility.AccessibilityManager; import androidx.annotation.ColorInt; -import androidx.core.content.ContextCompat; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.FloatingHeaderRow; import com.android.launcher3.allapps.FloatingHeaderView; +import com.android.launcher3.util.Themes; /** * A view which shows a horizontal divider @@ -84,10 +84,9 @@ public class AppsDividerView extends View implements FloatingHeaderRow { getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height) }; - mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant); + mStrokeColor = Themes.getAttrColor(context, R.attr.materialColorOutlineVariant); - mAllAppsLabelTextColor = ContextCompat.getColor(context, - R.color.material_color_on_surface_variant); + mAllAppsLabelTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurfaceVariant); mAccessibilityManager = AccessibilityManager.getInstance(context); setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context)); diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java index a16031d804..92d9516836 100644 --- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java +++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java @@ -290,6 +290,9 @@ public class PredictionRowView writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled); writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused); writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow); - writer.println(prefix + "\tmPredictedApps: " + mPredictedApps); + writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size()); + for (WorkspaceItemInfo info : mPredictedApps) { + writer.println(prefix + "\t\t" + info); + } } } diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 9178062c2d..f7da34a77f 100644 --- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt +++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt @@ -30,7 +30,7 @@ import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.quickstep.SystemUiProxy import com.android.quickstep.TaskViewUtils import com.android.quickstep.views.DesktopTaskView -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import java.util.function.Consumer /** Manage recents related operations with desktop tasks */ @@ -44,11 +44,13 @@ class DesktopRecentsTransitionController( /** Launch desktop tasks from recents view */ fun launchDesktopFromRecents( desktopTaskView: DesktopTaskView, + animated: Boolean, callback: Consumer? = null ) { val animRunner = RemoteDesktopLaunchTransitionRunner( desktopTaskView, + animated, stateManager, depthController, callback @@ -64,6 +66,7 @@ class DesktopRecentsTransitionController( private class RemoteDesktopLaunchTransitionRunner( private val desktopTaskView: DesktopTaskView, + private val animated: Boolean, private val stateManager: StateManager<*, *>, private val depthController: DepthController?, private val successCallback: Consumer? @@ -84,16 +87,21 @@ class DesktopRecentsTransitionController( } MAIN_EXECUTOR.execute { - TaskViewUtils.composeRecentsDesktopLaunchAnimator( - desktopTaskView, - stateManager, - depthController, - info, - t - ) { - errorHandlingFinishCallback.run() - successCallback?.accept(true) + val animator = + TaskViewUtils.composeRecentsDesktopLaunchAnimator( + desktopTaskView, + stateManager, + depthController, + info, + t + ) { + errorHandlingFinishCallback.run() + successCallback?.accept(true) + } + if (!animated) { + animator.setDuration(0) } + animator.start() } } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index de974ecae1..c50e82dd91 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -536,6 +536,9 @@ public class HotseatPredictionController implements DragController.DragListener, writer.println(prefix + "HotseatPredictionController"); writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags)); writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount); - writer.println(prefix + "\tmPredictedItems: " + mPredictedItems); + writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size()); + for (ItemInfo info : mPredictedItems) { + writer.println(prefix + "\t\t" + info); + } } } diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 8b5ed7cc4b..6af5a30b07 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -205,6 +205,7 @@ public class QuickstepModelDelegate extends ModelDelegate { mActive = true; } + @WorkerThread @Override public void workspaceLoadComplete() { super.workspaceLoadComplete(); @@ -323,6 +324,7 @@ public class QuickstepModelDelegate extends ModelDelegate { } } + @WorkerThread @Override public void destroy() { super.destroy(); diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java index a7c965218d..fb17f15925 100644 --- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java +++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java @@ -83,10 +83,8 @@ public final class WellbeingModel implements SafeCloseable { private final Handler mWorkerHandler; private final ContentObserver mContentObserver; - private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver = - new SimpleBroadcastReceiver(t -> restartObserver()); - private final SimpleBroadcastReceiver mAppAddRemoveReceiver = - new SimpleBroadcastReceiver(this::onAppPackageChanged); + private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver; + private final SimpleBroadcastReceiver mAppAddRemoveReceiver; private final Object mModelLock = new Object(); // Maps the action Id to the corresponding RemoteAction @@ -101,6 +99,11 @@ public final class WellbeingModel implements SafeCloseable { mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg) ? Executors.UI_HELPER_EXECUTOR.getLooper() : Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper()); + mWellbeingAppChangeReceiver = + new SimpleBroadcastReceiver(mWorkerHandler, t -> restartObserver()); + mAppAddRemoveReceiver = + new SimpleBroadcastReceiver(mWorkerHandler, this::onAppPackageChanged); + mContentObserver = new ContentObserver(mWorkerHandler) { @Override @@ -111,6 +114,7 @@ public final class WellbeingModel implements SafeCloseable { mWorkerHandler.post(this::initializeInBackground); } + @WorkerThread private void initializeInBackground() { if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { mContext.registerReceiver( diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java index 84313965e6..8c98babc0a 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java +++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java @@ -40,7 +40,6 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider; @@ -60,25 +59,31 @@ import java.util.stream.Collectors; public class WidgetPredictionsRequester { private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; + // container/screenid/[positionx,positiony]/[spanx,spany] + // Matches the format passed used by PredictionHelper; But, position and size values aren't + // used, so, we pass default values. + @VisibleForTesting + static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]"; @Nullable private AppPredictor mAppPredictor; private final Context mContext; @NonNull private final String mUiSurface; + private boolean mPredictionsAvailable; @NonNull - private final Map> mAllWidgets; + private final Map mAllWidgets; public WidgetPredictionsRequester(Context context, @NonNull String uiSurface, - @NonNull Map> allWidgets) { + @NonNull Map allWidgets) { mContext = context; mUiSurface = uiSurface; mAllWidgets = Collections.unmodifiableMap(allWidgets); } /** - * Requests predictions from the app predictions manager and registers the provided callback to - * receive updates when predictions are available. + * Requests one time predictions from the app predictions manager and invokes provided callback + * once predictions are available. * * @param existingWidgets widgets that are currently added to the surface; * @param callback consumer of prediction results to be called when predictions are @@ -86,7 +91,7 @@ public class WidgetPredictionsRequester { */ public void request(List existingWidgets, Consumer> callback) { - Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface); + Bundle bundle = buildBundleForPredictionSession(existingWidgets); Predicate filter = notOnUiSurfaceFilter(existingWidgets); MODEL_EXECUTOR.execute(() -> { @@ -112,17 +117,14 @@ public class WidgetPredictionsRequester { * Returns a bundle that can be passed in a prediction session * * @param addedWidgets widgets that are already added by the user in the ui surface - * @param uiSurface a unique identifier of the surface hosting widgets; format - * "widgets_xx"; note - "widgets" is reserved for home screen surface. */ @VisibleForTesting - static Bundle buildBundleForPredictionSession(List addedWidgets, - String uiSurface) { + static Bundle buildBundleForPredictionSession(List addedWidgets) { Bundle bundle = new Bundle(); ArrayList addedAppTargetEvents = new ArrayList<>(); for (AppWidgetProviderInfo info : addedWidgets) { ComponentName componentName = info.provider; - AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName); + AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName); addedAppTargetEvents.add(appTargetEvent); } bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents); @@ -134,13 +136,13 @@ public class WidgetPredictionsRequester { * predictor. * Also see {@link PredictionHelper} */ - private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, + private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info, ComponentName componentName) { AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName()); AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(), /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build(); - return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) - .setLaunchLocation(uiSurface).build(); + return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation( + LAUNCH_LOCATION).build(); } /** @@ -160,10 +162,14 @@ public class WidgetPredictionsRequester { @WorkerThread private void bindPredictions(List targets, Predicate filter, Consumer> callback) { - List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); - List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); + if (!mPredictionsAvailable) { + mPredictionsAvailable = true; + List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); + List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); - MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); + MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); + MODEL_EXECUTOR.execute(this::clear); + } } /** @@ -172,33 +178,19 @@ public class WidgetPredictionsRequester { */ @VisibleForTesting static List filterPredictions(List predictions, - Map> allWidgets, Predicate filter) { + Map allWidgets, Predicate filter) { List servicePredictedItems = new ArrayList<>(); - List localFilteredWidgets = new ArrayList<>(); for (AppTarget prediction : predictions) { - List widgetsInPackage = allWidgets.get( - new PackageUserKey(prediction.getPackageName(), prediction.getUser())); - if (widgetsInPackage == null || widgetsInPackage.isEmpty()) { - continue; - } String className = prediction.getClassName(); if (!TextUtils.isEmpty(className)) { - WidgetItem item = widgetsInPackage.stream() - .filter(w -> className.equals(w.componentName.getClassName())) - .filter(filter) - .findFirst().orElse(null); - if (item != null) { - servicePredictedItems.add(item); - continue; + WidgetItem widgetItem = allWidgets.get( + new ComponentKey(new ComponentName(prediction.getPackageName(), className), + prediction.getUser())); + if (widgetItem != null && filter.test(widgetItem)) { + servicePredictedItems.add(widgetItem); } } - // No widget was added by the service, try local filtering - widgetsInPackage.stream().filter(filter).findFirst() - .ifPresent(localFilteredWidgets::add); - } - if (servicePredictedItems.isEmpty()) { - servicePredictedItems.addAll(localFilteredWidgets); } return servicePredictedItems; @@ -229,5 +221,6 @@ public class WidgetPredictionsRequester { mAppPredictor.destroy(); mAppPredictor = null; } + mPredictionsAvailable = false; } } diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index 64bb05e1ab..0395d32c13 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -65,7 +65,7 @@ public final class WidgetsPredictionUpdateTask implements ModelUpdateTask { Collectors.toSet()); Predicate notOnWorkspace = w -> !widgetsInWorkspace.contains(w); Map allWidgets = - dataModel.widgetsModel.getAllWidgetComponentsWithoutShortcuts(); + dataModel.widgetsModel.getWidgetsByComponentKey(); List servicePredictedItems = new ArrayList<>(); diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 747612d823..4c24d95462 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -19,6 +19,7 @@ package com.android.launcher3.statehandlers; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH; import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import android.animation.Animator; @@ -74,8 +75,9 @@ public class DepthController extends BaseDepthController implements StateHandler mOnAttachListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance().addListener( + mLauncher.getMainExecutor(), mCrossWindowBlurListener)); mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); // To handle the case where window token is invalid during last setDepth call. @@ -108,7 +110,9 @@ public class DepthController extends BaseDepthController implements StateHandler private void removeSecondaryListeners() { if (mCrossWindowBlurListener != null) { - CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance() + .removeListener(mCrossWindowBlurListener)); } if (mOpaquenessListener != null) { mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java index 62cc0bb90a..3dcb2ac0e6 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -16,27 +16,32 @@ package com.android.launcher3.statehandlers; import static android.view.View.VISIBLE; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; -import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity; +import android.content.Context; import android.os.Debug; -import android.os.SystemProperties; import android.util.Log; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; +import com.android.launcher3.views.ActivityContext; import com.android.quickstep.GestureState; import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.fallback.RecentsState; import com.android.wm.shell.desktopmode.IDesktopTaskListener; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import java.io.PrintWriter; import java.util.HashSet; import java.util.Set; @@ -48,8 +53,8 @@ public class DesktopVisibilityController { private static final String TAG = "DesktopVisController"; private static final boolean DEBUG = false; - private final Launcher mLauncher; private final Set mDesktopVisibilityListeners = new HashSet<>(); + private final Set mTaskbarDesktopModeListeners = new HashSet<>(); private int mVisibleDesktopTasksCount; private boolean mInOverviewState; @@ -57,42 +62,46 @@ public class DesktopVisibilityController { private boolean mGestureInProgress; @Nullable - private IDesktopTaskListener mDesktopTaskListener; + private DesktopTaskListenerImpl mDesktopTaskListener; - public DesktopVisibilityController(Launcher launcher) { - mLauncher = launcher; + @Nullable + private Context mContext; + + public DesktopVisibilityController(@NonNull Context context) { + setContext(context); } - /** - * Register a listener with System UI to receive updates about desktop tasks state - */ - public void registerSystemUiListener() { - mDesktopTaskListener = new IDesktopTaskListener.Stub() { - @Override - public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) { - MAIN_EXECUTOR.execute(() -> { - if (displayId == mLauncher.getDisplayId()) { - if (DEBUG) { - Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount); - } - setVisibleDesktopTasksCount(visibleTasksCount); - } - }); - } + /** Sets the context and re-registers the System Ui listener */ + private void setContext(@Nullable Context context) { + unregisterSystemUiListener(); + mContext = context; + registerSystemUiListener(); + } - @Override - public void onStashedChanged(int displayId, boolean stashed) { - Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated"); - } - }; - SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener); + /** Register a listener with System UI to receive updates about desktop tasks state */ + private void registerSystemUiListener() { + if (mContext == null) { + return; + } + if (mDesktopTaskListener != null) { + return; + } + mDesktopTaskListener = new DesktopTaskListenerImpl(this, mContext.getDisplayId()); + SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(mDesktopTaskListener); } /** * Clear listener from System UI that was set with {@link #registerSystemUiListener()} */ - public void unregisterSystemUiListener() { - SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null); + private void unregisterSystemUiListener() { + if (mContext == null) { + return; + } + if (mDesktopTaskListener == null) { + return; + } + SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(null); + mDesktopTaskListener.release(); mDesktopTaskListener = null; } @@ -125,11 +134,24 @@ public class DesktopVisibilityController { mDesktopVisibilityListeners.remove(listener); } + /** Registers a listener for Taskbar changes in Desktop Mode. */ + public void registerTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) { + mTaskbarDesktopModeListeners.add(listener); + } + + /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */ + public void unregisterTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) { + mTaskbarDesktopModeListeners.remove(listener); + } + /** * Sets the number of desktop windows that are visible and updates launcher visibility based on * it. */ public void setVisibleDesktopTasksCount(int visibleTasksCount) { + if (mContext == null) { + return; + } if (DEBUG) { Log.d(TAG, "setVisibleDesktopTasksCount: visibleTasksCount=" + visibleTasksCount + " currentValue=" + mVisibleDesktopTasksCount); @@ -145,7 +167,8 @@ public class DesktopVisibilityController { notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow); } - if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) { + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + && wasVisible != isVisible) { // TODO: b/333533253 - Remove after flag rollout if (mVisibleDesktopTasksCount > 0) { setLauncherViewsVisibility(View.INVISIBLE); @@ -164,19 +187,33 @@ public class DesktopVisibilityController { } } + public void onLauncherStateChanged(LauncherState state) { + onLauncherStateChanged( + state, state == LauncherState.BACKGROUND_APP, state.isRecentsViewVisible); + } + + public void onLauncherStateChanged(RecentsState state) { + onLauncherStateChanged( + state, state == RecentsState.BACKGROUND_APP, state.isRecentsViewVisible()); + } + /** * Process launcher state change and update launcher view visibility based on desktop state */ - public void onLauncherStateChanged(LauncherState state) { + public void onLauncherStateChanged( + BaseState state, boolean isBackgroundAppState, boolean isRecentsViewVisible) { if (DEBUG) { Log.d(TAG, "onLauncherStateChanged: newState=" + state); } - setBackgroundStateEnabled(state == BACKGROUND_APP); + setBackgroundStateEnabled(isBackgroundAppState); // Desktop visibility tracks overview and background state separately - setOverviewStateEnabled(state != BACKGROUND_APP && state.isRecentsViewVisible); + setOverviewStateEnabled(!isBackgroundAppState && isRecentsViewVisible); } private void setOverviewStateEnabled(boolean overviewStateEnabled) { + if (mContext == null) { + return; + } if (DEBUG) { Log.d(TAG, "setOverviewStateEnabled: enabled=" + overviewStateEnabled + " currentValue=" + mInOverviewState); @@ -189,7 +226,7 @@ public class DesktopVisibilityController { notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow); } - if (enableDesktopWindowingWallpaperActivity()) { + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { return; } // TODO: b/333533253 - Clean up after flag rollout @@ -207,13 +244,26 @@ public class DesktopVisibilityController { } private void notifyDesktopVisibilityListeners(boolean areDesktopTasksVisible) { + if (mContext == null) { + return; + } if (DEBUG) { Log.d(TAG, "notifyDesktopVisibilityListeners: visible=" + areDesktopTasksVisible); } for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) { listener.onDesktopVisibilityChanged(areDesktopTasksVisible); } - DisplayController.handleInfoChangeForDesktopMode(mLauncher); + DisplayController.INSTANCE.get(mContext).notifyConfigChange(); + } + + private void notifyTaskbarDesktopModeListeners(boolean doesAnyTaskRequireTaskbarRounding) { + if (DEBUG) { + Log.d(TAG, "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding=" + + doesAnyTaskRequireTaskbarRounding); + } + for (TaskbarDesktopModeListener listener : mTaskbarDesktopModeListeners) { + listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding); + } } /** @@ -289,22 +339,32 @@ public class DesktopVisibilityController { * TODO: b/333533253 - Remove after flag rollout */ private void setLauncherViewsVisibility(int visibility) { - if (enableDesktopWindowingWallpaperActivity()) { + if (mContext == null) { + return; + } + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { return; } if (DEBUG) { Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility + " " + Debug.getCaller()); } - View workspaceView = mLauncher.getWorkspace(); - if (workspaceView != null) { - workspaceView.setVisibility(visibility); + if (!(mContext instanceof ActivityContext activity)) { + return; } - View dragLayer = mLauncher.getDragLayer(); + View dragLayer = activity.getDragLayer(); if (dragLayer != null) { dragLayer.setVisibility(visibility); } - if (mLauncher instanceof QuickstepLauncher ql && ql.getTaskbarUIController() != null + if (!(activity instanceof Launcher launcher)) { + return; + } + View workspaceView = launcher.getWorkspace(); + if (workspaceView != null) { + workspaceView.setVisibility(visibility); + } + if (launcher instanceof QuickstepLauncher ql + && ql.getTaskbarUIController() != null && mVisibleDesktopTasksCount != 0) { ql.getTaskbarUIController().onLauncherVisibilityChanged(visibility == VISIBLE); } @@ -314,7 +374,10 @@ public class DesktopVisibilityController { * TODO: b/333533253 - Remove after flag rollout */ private void markLauncherPaused() { - if (enableDesktopWindowingWallpaperActivity()) { + if (mContext == null) { + return; + } + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { return; } if (DEBUG) { @@ -331,7 +394,10 @@ public class DesktopVisibilityController { * TODO: b/333533253 - Remove after flag rollout */ private void markLauncherResumed() { - if (enableDesktopWindowingWallpaperActivity()) { + if (mContext == null) { + return; + } + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { return; } if (DEBUG) { @@ -347,6 +413,22 @@ public class DesktopVisibilityController { } } + public void onDestroy() { + setContext(null); + } + + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "DesktopVisibilityController:"); + + pw.println(prefix + "\tmDesktopVisibilityListeners=" + mDesktopVisibilityListeners); + pw.println(prefix + "\tmVisibleDesktopTasksCount=" + mVisibleDesktopTasksCount); + pw.println(prefix + "\tmInOverviewState=" + mInOverviewState); + pw.println(prefix + "\tmBackgroundStateEnabled=" + mBackgroundStateEnabled); + pw.println(prefix + "\tmGestureInProgress=" + mGestureInProgress); + pw.println(prefix + "\tmDesktopTaskListener=" + mDesktopTaskListener); + pw.println(prefix + "\tmContext=" + mContext); + } + /** A listener for when the user enters/exits Desktop Mode. */ public interface DesktopVisibilityListener { /** @@ -356,4 +438,65 @@ public class DesktopVisibilityController { */ void onDesktopVisibilityChanged(boolean visible); } + + /** + * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher + * activity via the controller. + */ + private static class DesktopTaskListenerImpl extends IDesktopTaskListener.Stub { + + private DesktopVisibilityController mController; + private final int mDisplayId; + + DesktopTaskListenerImpl(@NonNull DesktopVisibilityController controller, int displayId) { + mController = controller; + mDisplayId = displayId; + } + + /** + * Clears any references to the controller. + */ + void release() { + mController = null; + } + + @Override + public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) { + MAIN_EXECUTOR.execute(() -> { + if (mController != null && displayId == mDisplayId) { + if (DEBUG) { + Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount); + } + mController.setVisibleDesktopTasksCount(visibleTasksCount); + } + }); + } + + @Override + public void onStashedChanged(int displayId, boolean stashed) { + Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated"); + } + + @Override + public void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding) { + MAIN_EXECUTOR.execute(() -> { + if (mController != null && DesktopModeStatus.useRoundedCorners()) { + Log.d(TAG, "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= " + + doesAnyTaskRequireTaskbarRounding); + mController.notifyTaskbarDesktopModeListeners( + doesAnyTaskRequireTaskbarRounding); + } + }); + } + } + + /** A listener for Taskbar in Desktop Mode. */ + public interface TaskbarDesktopModeListener { + /** + * Callback for when task is resized in desktop mode. + * + * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness. + */ + void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java index c201236586..a833ccf9c4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java @@ -16,19 +16,24 @@ package com.android.launcher3.taskbar; import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; +import com.android.quickstep.SystemUiProxy; import java.util.ArrayList; import java.util.List; // TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar. /** Base for common behavior between taskbar window contexts. */ -public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext { +public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext, + SystemShortcut.BubbleActivityStarter { protected final LayoutInflater mLayoutInflater; private final List mDPChangeListeners = new ArrayList<>(); @@ -48,6 +53,18 @@ public abstract class BaseTaskbarContext extends ContextThemeWrapper implements return mDPChangeListeners; } + @Override + public void showShortcutBubble(ShortcutInfo info) { + if (info == null) return; + SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info); + } + + @Override + public void showAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) return; + SystemUiProxy.INSTANCE.get(this).showAppBubble(intent); + } + /** Callback invoked when a drag is initiated within this context. */ public abstract void onDragStart(); diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java index 06d9ee6c96..929e7936e0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java @@ -70,7 +70,6 @@ public class FallbackTaskbarUIController extends TaskbarUIController { @Override protected void init(TaskbarControllers taskbarControllers) { super.init(taskbarControllers); - mRecentsActivity.setTaskbarUIController(this); mRecentsActivity.getStateManager().addStateListener(mStateListener); } @@ -78,6 +77,7 @@ public class FallbackTaskbarUIController extends TaskbarUIController { @Override protected void onDestroy() { super.onDestroy(); + getRecentsView().setTaskLaunchListener(null); mRecentsActivity.setTaskbarUIController(null); mRecentsActivity.getStateManager().removeStateListener(mStateListener); } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java index 358d703b75..ea432f3d28 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -23,12 +23,11 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.RecentsModel; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.LayoutUtils; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -66,6 +65,9 @@ public final class KeyboardQuickSwitchController implements @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; + private boolean mHasDesktopTask = false; + private boolean mWasDesktopTaskFilteredOut = false; + /** Initialize the controller. */ public void init(@NonNull TaskbarControllers controllers) { mControllers = controllers; @@ -112,10 +114,8 @@ public final class KeyboardQuickSwitchController implements mQuickSwitchViewController = new KeyboardQuickSwitchViewController( mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); - DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); final boolean onDesktop = - desktopController != null && desktopController.areDesktopTasksVisible(); + mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible(); if (mModel.isTaskListValid(mTaskListChangeId)) { // When we are opening the KQS with no focus override, check if the first task is @@ -126,11 +126,15 @@ public final class KeyboardQuickSwitchController implements /* updateTasks= */ false, currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() ? 0 : currentFocusedIndex, - onDesktop); + onDesktop, + mHasDesktopTask, + mWasDesktopTaskFilteredOut); return; } mTaskListChangeId = mModel.getTasks((tasks) -> { + mHasDesktopTask = false; + mWasDesktopTaskFilteredOut = false; if (onDesktop) { processLoadedTasksOnDesktop(tasks); } else { @@ -144,7 +148,9 @@ public final class KeyboardQuickSwitchController implements /* updateTasks= */ true, currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() ? 0 : currentFocusedIndex, - onDesktop); + onDesktop, + mHasDesktopTask, + mWasDesktopTaskFilteredOut); }); } @@ -152,9 +158,22 @@ public final class KeyboardQuickSwitchController implements // Only store MAX_TASK tasks, from most to least recent Collections.reverse(tasks); mTasks = tasks.stream() + .filter(task -> !(task instanceof DesktopTask)) .limit(MAX_TASKS) .collect(Collectors.toList()); - mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); + + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i) instanceof DesktopTask) { + mHasDesktopTask = true; + if (i < mTasks.size()) { + mWasDesktopTaskFilteredOut = true; + } + break; + } + } + + mNumHiddenTasks = Math.max(0, + tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS); } private void processLoadedTasksOnDesktop(List tasks) { @@ -214,6 +233,8 @@ public final class KeyboardQuickSwitchController implements pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); + pw.println(prefix + "\tmHasDesktopTask=" + mHasDesktopTask); + pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut); pw.println(prefix + "\tmTasks=["); for (GroupTask task : mTasks) { Task task1 = task.task1; @@ -235,21 +256,26 @@ public final class KeyboardQuickSwitchController implements class ControllerCallbacks { - int getTaskCount() { - return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1); - } - @Nullable GroupTask getTaskAt(int index) { return index < 0 || index >= mTasks.size() ? null : mTasks.get(index); } void updateThumbnailInBackground(Task task, Consumer callback) { - mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); + mModel.getThumbnailCache().getThumbnailInBackground(task, + thumbnailData -> { + task.thumbnail = thumbnailData; + callback.accept(thumbnailData); + }); } void updateIconInBackground(Task task, Consumer callback) { - mModel.getIconCache().updateIconInBackground(task, callback); + mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> { + task.icon = icon; + task.titleDescription = contentDescription; + task.title = title; + callback.accept(task); + }); } void onCloseComplete() { @@ -270,5 +296,10 @@ public final class KeyboardQuickSwitchController implements boolean isFirstTaskRunning() { return isTaskRunning(getTaskAt(0)); } + + boolean isAspectRatioSquare() { + return mControllers != null && LayoutUtils.isAspectRatioSquare( + mControllers.taskbarActivityContext.getDeviceProfile().aspectRatio); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java index 39b4f77ffe..ce9655646a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java @@ -36,22 +36,25 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; import com.android.launcher3.util.Preconditions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.quickstep.util.BorderAnimator; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; -import java.util.function.Consumer; - import kotlin.Unit; +import java.util.function.Consumer; + /** * A view that displays a recent task during a keyboard quick switch. */ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { private static final float THUMBNAIL_BLUR_RADIUS = 1f; + private static final int INVALID_BORDER_RADIUS = -1; @ColorInt private final int mBorderColor; + @ColorInt private final int mBorderRadius; @Nullable private BorderAnimator mBorderAnimator; @@ -87,6 +90,8 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { mBorderColor = ta.getColor( R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR); + mBorderRadius = ta.getDimensionPixelSize( + R.styleable.TaskView_focusBorderRadius, INVALID_BORDER_RADIUS); ta.recycle(); } @@ -103,8 +108,10 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { Preconditions.assertNotNull(mContent); mBorderAnimator = BorderAnimator.createScalingBorderAnimator( - /* borderRadiusPx= */ resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_task_view_radius), + /* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS + ? mBorderRadius + : resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius), /* borderWidthPx= */ resources.getDimensionPixelSize( R.dimen.keyboard_quick_switch_border_width), /* boundsBuilder= */ bounds -> { @@ -167,6 +174,61 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { }); } + protected void setThumbnailsForSplitTasks( + @NonNull Task task1, + @Nullable Task task2, + @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction, + @Nullable IconUpdateFunction iconUpdateFunction, + @Nullable SplitBounds splitBounds) { + setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction); + + if (splitBounds == null) { + return; + } + + + final boolean isLeftRightSplit = !splitBounds.appsStackedVertically; + final float leftOrTopTaskPercent = isLeftRightSplit + ? splitBounds.leftTaskPercent : splitBounds.topTaskPercent; + + ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams) + mThumbnailView1.getLayoutParams(); + ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams) + mThumbnailView2.getLayoutParams(); + + if (isLeftRightSplit) { + // Set thumbnail view ratio in left right split mode. + leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio. + leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT; + leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent; + leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; + leftTopParams.rightToLeft = R.id.thumbnail_2; + mThumbnailView1.setLayoutParams(leftTopParams); + + rightBottomParams.width = 0; + rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT; + rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent; + rightBottomParams.leftToRight = R.id.thumbnail_1; + rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; + mThumbnailView2.setLayoutParams(rightBottomParams); + } else { + // Set thumbnail view ratio in top bottom split mode. + leftTopParams.height = 0; + leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT; + leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent; + leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; + leftTopParams.bottomToTop = R.id.thumbnail_2; + mThumbnailView1.setLayoutParams(leftTopParams); + + rightBottomParams.height = 0; + rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT; + rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent; + rightBottomParams.topToBottom = R.id.thumbnail_1; + rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + mThumbnailView2.setLayoutParams(rightBottomParams); + } + } + private void applyThumbnail( @Nullable ImageView thumbnailView, @Nullable Task task, diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java index 5d47212114..fc8204aa91 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -36,31 +36,34 @@ import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.widget.HorizontalScrollView; -import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.res.ResourcesCompat; import com.android.app.animation.Interpolators; +import com.android.internal.jank.Cuj; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import java.util.HashMap; import java.util.List; import java.util.Locale; /** - * View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab - * commands. + * View that allows quick switching between recent tasks. + * + * Can be access via: + * - keyboard alt-tab + * - alt-shift-tab + * - taskbar overflow button */ public class KeyboardQuickSwitchView extends ConstraintLayout { @@ -99,9 +102,13 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private int mTaskViewWidth; private int mTaskViewHeight; private int mSpacing; + private int mSmallSpacing; private int mOutlineRadius; private boolean mIsRtl; + private int mOverviewTaskIndex = -1; + private int mDesktopTaskIndex = -1; + @Nullable private AnimatorSet mOpenAnimation; @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; @@ -138,6 +145,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { mTaskViewHeight = resources.getDimensionPixelSize( R.dimen.keyboard_quick_switch_taskview_height); mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); + mSmallSpacing = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_view_small_spacing); mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); mIsRtl = Utilities.isRtl(resources); } @@ -145,6 +154,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private KeyboardQuickSwitchTaskView createAndAddTaskView( int index, boolean isFinalView, + boolean useSmallStartSpacing, @LayoutRes int resId, @NonNull LayoutInflater layoutInflater, @Nullable View previousView) { @@ -153,7 +163,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { taskView.setId(View.generateViewId()); taskView.setOnClickListener(v -> mViewCallbacks.launchTaskAt(index)); - LayoutParams lp = new LayoutParams(mTaskViewWidth, mTaskViewHeight); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); // Create a left-to-right ordering of views (or right-to-left in RTL locales) if (previousView != null) { lp.startToEnd = previousView.getId(); @@ -163,7 +173,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { lp.topToTop = PARENT_ID; lp.bottomToBottom = PARENT_ID; // Add spacing between views - lp.setMarginStart(mSpacing); + lp.setMarginStart(useSmallStartSpacing ? mSmallSpacing : mSpacing); if (isFinalView) { // Add spacing to the end of the final view so that scrolling ends with some padding. lp.endToEnd = PARENT_ID; @@ -182,7 +192,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, - @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) { + @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks, + boolean useDesktopTaskView) { mViewCallbacks = viewCallbacks; Resources resources = context.getResources(); Resources.Theme theme = context.getTheme(); @@ -194,50 +205,64 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { GroupTask groupTask = groupTasks.get(i); KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView( i, - /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0, - groupTask instanceof DesktopTask - ? R.layout.keyboard_quick_switch_textonly_taskview + /* isFinalView= */ i == tasksToDisplay - 1 + && numHiddenTasks == 0 && !useDesktopTaskView, + /* useSmallStartSpacing= */ false, + mViewCallbacks.isAspectRatioSquare() + ? R.layout.keyboard_quick_switch_taskview_square : R.layout.keyboard_quick_switch_taskview, layoutInflater, previousTaskView); - if (groupTask instanceof DesktopTask desktopTask) { - HashMap args = new HashMap<>(); - args.put("count", desktopTask.tasks.size()); + final boolean firstTaskIsLeftTopTask = + groupTask.mSplitBounds == null + || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id + || groupTask.task2 == null; + + currentTaskView.setThumbnailsForSplitTasks( + firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2, + firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1, + updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, + updateTasks ? mViewCallbacks::updateIconInBackground : null, + groupTask.mSplitBounds); - currentTaskView.findViewById(R.id.icon).setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.ic_desktop, theme)); - currentTaskView.findViewById(R.id.text).setText(new MessageFormat( - resources.getString(R.string.quick_switch_desktop), - Locale.getDefault()).format(args)); - } else { - currentTaskView.setThumbnails( - groupTask.task1, - groupTask.task2, - updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, - updateTasks ? mViewCallbacks::updateIconInBackground : null); - } previousTaskView = currentTaskView; } - if (numHiddenTasks > 0) { HashMap args = new HashMap<>(); args.put("count", numHiddenTasks); + mOverviewTaskIndex = getTaskCount(); View overviewButton = createAndAddTaskView( - MAX_TASKS, - /* isFinalView= */ true, - R.layout.keyboard_quick_switch_textonly_taskview, + mOverviewTaskIndex, + /* isFinalView= */ !useDesktopTaskView, + /* useSmallStartSpacing= */ false, + R.layout.keyboard_quick_switch_overview_taskview, layoutInflater, previousTaskView); - overviewButton.findViewById(R.id.icon).setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.view_carousel, theme)); - overviewButton.findViewById(R.id.text).setText(new MessageFormat( + overviewButton.findViewById(R.id.large_text).setText( + String.format(Locale.getDefault(), "%d", numHiddenTasks)); + overviewButton.findViewById(R.id.small_text).setText(new MessageFormat( resources.getString(R.string.quick_switch_overflow), Locale.getDefault()).format(args)); + + previousTaskView = overviewButton; } - mDisplayingRecentTasks = !groupTasks.isEmpty(); + if (useDesktopTaskView) { + mDesktopTaskIndex = getTaskCount(); + View desktopButton = createAndAddTaskView( + mDesktopTaskIndex, + /* isFinalView= */ true, + /* useSmallStartSpacing= */ numHiddenTasks > 0, + R.layout.keyboard_quick_switch_desktop_taskview, + layoutInflater, + previousTaskView); + + desktopButton.findViewById(R.id.text).setText( + resources.getString(R.string.quick_switch_desktop)); + } + mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView; getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @@ -250,6 +275,14 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { }); } + int getOverviewTaskIndex() { + return mOverviewTaskIndex; + } + + int getDesktopTaskIndex() { + return mDesktopTaskIndex; + } + protected Animator getCloseAnimation() { AnimatorSet closeAnimation = new AnimatorSet(); @@ -331,6 +364,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); + InteractionJankMonitorWrapper.begin( + KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); setClipToPadding(false); setOutlineProvider(new ViewOutlineProvider() { @Override @@ -358,13 +393,19 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { } }); animateFocusMove(-1, Math.min( - mContent.getChildCount() - 1, + getTaskCount() - 1, currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride)); displayedContent.setVisibility(VISIBLE); setVisibility(VISIBLE); requestFocus(); } + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); + } + @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); @@ -372,6 +413,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { setOutlineProvider(outlineProvider); invalidateOutline(); mOpenAnimation = null; + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); } }); @@ -558,7 +600,11 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { @Nullable protected KeyboardQuickSwitchTaskView getTaskAt(int index) { - return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount() + return !mDisplayingRecentTasks || index < 0 || index >= getTaskCount() ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); } + + public int getTaskCount() { + return mContent.getChildCount(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java index d6ee92f195..40e77e2394 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -15,12 +15,10 @@ */ package com.android.launcher3.taskbar; -import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED; - import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.animation.Animator; -import android.app.ActivityOptions; +import android.animation.AnimatorListenerAdapter; import android.view.KeyEvent; import android.view.animation.AnimationUtils; import android.window.RemoteTransition; @@ -28,16 +26,16 @@ import android.window.RemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.jank.Cuj; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.SlideInRemoteTransition; 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.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.QuickStepContract; import java.io.PrintWriter; @@ -61,6 +59,7 @@ public class KeyboardQuickSwitchViewController { private int mCurrentFocusIndex = -1; private boolean mOnDesktop; + private boolean mWasDesktopTaskFilteredOut; protected KeyboardQuickSwitchViewController( @NonNull TaskbarControllers controllers, @@ -82,9 +81,12 @@ public class KeyboardQuickSwitchViewController { int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, - boolean onDesktop) { + boolean onDesktop, + boolean hasDesktopTask, + boolean wasDesktopTaskFilteredOut) { mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView); mOnDesktop = onDesktop; + mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut; mKeyboardQuickSwitchView.applyLoadPlan( mOverlayContext, @@ -92,7 +94,8 @@ public class KeyboardQuickSwitchViewController { numHiddenTasks, updateTasks, currentFocusIndexOverride, - mViewCallbacks); + mViewCallbacks, + /* useDesktopTaskView= */ !onDesktop && hasDesktopTask); } boolean isCloseAnimationRunning() { @@ -101,18 +104,28 @@ public class KeyboardQuickSwitchViewController { protected void closeQuickSwitchView(boolean animate) { if (isCloseAnimationRunning()) { - // Let currently-running animation finish. if (!animate) { mCloseAnimation.end(); } + // Let currently-running animation finish. return; } if (!animate) { + InteractionJankMonitorWrapper.begin( + mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); onCloseComplete(); return; } mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation(); + mCloseAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + InteractionJankMonitorWrapper.begin( + mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); + } + }); mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete)); mCloseAnimation.start(); } @@ -131,7 +144,7 @@ public class KeyboardQuickSwitchViewController { } // If the user quick switches too quickly, updateCurrentFocusIndex might not have run. return launchTaskAt(mControllerCallbacks.isFirstTaskRunning() - && mControllerCallbacks.getTaskCount() > 1 ? 1 : 0); + && mKeyboardQuickSwitchView.getTaskCount() > 1 ? 1 : 0); } private int launchTaskAt(int index) { @@ -139,6 +152,33 @@ public class KeyboardQuickSwitchViewController { // Ignore taps on task views and alt key unpresses while the close animation is running. return -1; } + if (index == mKeyboardQuickSwitchView.getOverviewTaskIndex()) { + // If there is a desktop task view, then we should account for it when focusing the + // first hidden non-desktop task view in recents view + return mOnDesktop ? 1 : (mWasDesktopTaskFilteredOut ? index + 1 : index); + } + Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin( + mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH); + Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end( + Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH); + TaskbarActivityContext context = mControllers.taskbarActivityContext; + RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition( + Utilities.isRtl(mControllers.taskbarActivityContext.getResources()), + context.getDeviceProfile().overviewPageSpacing, + QuickStepContract.getWindowCornerRadius(context), + AnimationUtils.loadInterpolator( + context, android.R.interpolator.fast_out_extra_slow_in), + onStartCallback, + onFinishCallback), + "SlideInTransition"); + if (index == mKeyboardQuickSwitchView.getDesktopTaskIndex()) { + UI_HELPER_EXECUTOR.execute(() -> + SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext()) + .showDesktopApps( + mKeyboardQuickSwitchView.getDisplay().getDisplayId(), + remoteTransition)); + return -1; + } // Even with a valid index, this can be null if the user tries to quick switch before the // views have been added in the KeyboardQuickSwitchView. GroupTask task = mControllerCallbacks.getTaskAt(index); @@ -149,37 +189,12 @@ public class KeyboardQuickSwitchViewController { // Ignore attempts to run the selected task if it is already running. return -1; } - - TaskbarActivityContext context = mControllers.taskbarActivityContext; - RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition( - Utilities.isRtl(mControllers.taskbarActivityContext.getResources()), - context.getDeviceProfile().overviewPageSpacing, - QuickStepContract.getWindowCornerRadius(context), - AnimationUtils.loadInterpolator( - context, android.R.interpolator.fast_out_extra_slow_in)), - "SlideInTransition"); - if (task instanceof DesktopTask) { - UI_HELPER_EXECUTOR.execute(() -> - SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext()) - .showDesktopApps( - mKeyboardQuickSwitchView.getDisplay().getDisplayId(), - remoteTransition)); - } else if (mOnDesktop) { - UI_HELPER_EXECUTOR.execute(() -> - SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext()) - .showDesktopApp(task.task1.key.id)); - } else if (task.task2 == null) { - UI_HELPER_EXECUTOR.execute(() -> { - ActivityOptions activityOptions = mControllers.taskbarActivityContext - .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options; - activityOptions.setRemoteTransition(remoteTransition); - - ActivityManagerWrapper.getInstance().startActivityFromRecents( - task.task1.key, activityOptions); - }); - } else { - mControllers.uiController.launchSplitTasks(task, remoteTransition); - } + mControllers.taskbarActivityContext.handleGroupTaskLaunch( + task, + remoteTransition, + mOnDesktop, + onStartCallback, + onFinishCallback); return -1; } @@ -187,6 +202,7 @@ public class KeyboardQuickSwitchViewController { mCloseAnimation = null; mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); mControllerCallbacks.onCloseComplete(); + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); } protected void onDestroy() { @@ -199,6 +215,8 @@ public class KeyboardQuickSwitchViewController { pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus()); pw.println(prefix + "\tisCloseAnimationRunning=" + isCloseAnimationRunning()); pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex); + pw.println(prefix + "\tmOnDesktop=" + mOnDesktop); + pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut); } class ViewCallbacks { @@ -226,7 +244,7 @@ public class KeyboardQuickSwitchViewController { boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed()) || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL) || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL); - int taskCount = mControllerCallbacks.getTaskCount(); + int taskCount = mKeyboardQuickSwitchView.getTaskCount(); int toIndex = mCurrentFocusIndex == -1 // Focus the second-most recent app if possible ? (taskCount > 1 ? 1 : 0) @@ -261,5 +279,9 @@ public class KeyboardQuickSwitchViewController { void updateIconInBackground(Task task, Consumer callback) { mControllerCallbacks.updateIconInBackground(task, callback); } + + boolean isAspectRatioSquare() { + return mControllerCallbacks.isAspectRatioSquare(); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 779009a6b0..477f90cedf 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -15,12 +15,12 @@ */ package com.android.launcher3.taskbar; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; + import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION; import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE; import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES; import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; -import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity; import android.animation.Animator; import android.animation.AnimatorSet; @@ -38,20 +38,19 @@ import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.OnboardingPrefs; import com.android.quickstep.HomeVisibilityState; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TISBindHelper; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import java.io.PrintWriter; import java.util.Arrays; @@ -128,8 +127,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override protected void onDestroy() { + onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */); super.onDestroy(); - onLauncherVisibilityChanged(false); mTaskbarLauncherStateController.onDestroy(); mLauncher.setTaskbarUIController(null); @@ -160,6 +159,16 @@ public class LauncherTaskbarUIController extends TaskbarUIController { shouldDelayLauncherStateAnim); } + @Override + public void stashHotseat(boolean stash) { + mTaskbarLauncherStateController.stashHotseat(stash); + } + + @Override + public void unStashHotseatInstantly() { + mTaskbarLauncherStateController.unStashHotseatInstantly(); + } + /** * Adds the Launcher resume animator to the given animator set. * @@ -184,13 +193,16 @@ public class LauncherTaskbarUIController extends TaskbarUIController { */ @Override public void onLauncherVisibilityChanged(boolean isVisible) { + if (DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(mLauncher)) { + DisplayController.INSTANCE.get(mLauncher).notifyConfigChange(); + } onLauncherVisibilityChanged(isVisible, false /* fromInit */); } - private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInit) { + private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInitOrDestroy) { onLauncherVisibilityChanged( isVisible, - fromInit, + fromInitOrDestroy, /* startAnimation= */ true, DisplayController.isTransientTaskbar(mLauncher) ? TRANSIENT_TASKBAR_TRANSITION_DURATION @@ -201,27 +213,25 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Nullable private Animator onLauncherVisibilityChanged( - boolean isVisible, boolean fromInit, boolean startAnimation, int duration) { + boolean isVisible, boolean fromInitOrDestroy, boolean startAnimation, int duration) { // Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so // avoid updating taskbar state in that situation (when it's non-interactive -- or // "background") to avoid premature animations. - if (ENABLE_SHELL_TRANSITIONS && isVisible - && mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE) - && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) { + LauncherState state = mLauncher.getStateManager().getState(); + boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE) + && !state.isTaskbarAlignedWithHotseat(mLauncher); + if (isVisible && (nonInteractiveState || mSkipLauncherVisibilityChange)) { return null; } - DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (!enableDesktopWindowingWallpaperActivity() - && desktopController != null - && desktopController.areDesktopTasksVisible()) { + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) { // TODO: b/333533253 - Remove after flag rollout isVisible = false; } mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible); - if (fromInit) { + if (fromInitOrDestroy) { duration = 0; } return mTaskbarLauncherStateController.applyState(duration, startAnimation); @@ -262,7 +272,12 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } public boolean isDraggingItem() { - return mControllers.taskbarDragController.isDragging(); + boolean bubblesDragging = false; + if (mControllers.bubbleControllers.isPresent()) { + bubblesDragging = + mControllers.bubbleControllers.get().bubbleDragController.isDragging(); + } + return mControllers.taskbarDragController.isDragging() || bubblesDragging; } @Override @@ -452,4 +467,13 @@ public class LauncherTaskbarUIController extends TaskbarUIController { protected String getTaskbarUIControllerName() { return "LauncherTaskbarUIController"; } + + @Override + public void onSwipeToUnstashTaskbar() { + // Once taskbar is unstashed, the user cannot return back to the overlay. We can + // clear it here to set the expected state once the user goes home. + if (mLauncher.getWorkspace().isOverlayShown()) { + mLauncher.getWorkspace().onOverlayScrollChanged(0); + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 81581b8660..2ac579361e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -41,6 +41,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_B import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; @@ -62,7 +63,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Region.Op; -import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.graphics.drawable.RotateDrawable; @@ -73,11 +73,10 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.OnAttachStateChangeListener; -import android.view.View.OnClickListener; -import android.view.View.OnHoverListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.inputmethod.Flags; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -92,6 +91,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.anim.AlphaUpdateListener; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter; import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; @@ -104,9 +104,10 @@ import com.android.launcher3.views.BaseDragLayer; import com.android.systemui.shared.navigationbar.KeyButtonRipple; import com.android.systemui.shared.rotation.FloatingRotationButton; import com.android.systemui.shared.rotation.RotationButton; -import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.ArrayList; @@ -116,7 +117,8 @@ import java.util.function.IntPredicate; /** * Controller for managing nav bar buttons in taskbar */ -public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController { +public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController, + BubbleBarController.BubbleBarLocationListener { private final Rect mTempRect = new Rect(); @@ -153,6 +155,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT public static final int ALPHA_INDEX_SUW = 2; private static final int NUM_ALPHA_CHANNELS = 3; + private static final long AUTODIM_TIMEOUT_MS = 2250; + private final ArrayList mPropertyHolders = new ArrayList<>(); private final ArrayList mAllButtons = new ArrayList<>(); private int mState; @@ -161,6 +165,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private final @Nullable Context mNavigationBarPanelContext; private final WindowManagerProxy mWindowManagerProxy; private final NearestTouchFrame mNavButtonsView; + private final Handler mHandler; private final LinearLayout mNavButtonContainer; // Used for IME+A11Y buttons private final ViewGroup mEndContextualContainer; @@ -182,7 +187,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT this::updateNavButtonInAppDisplayProgressForSysui); /** Expected nav button dark intensity communicated via the framework. */ private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat( - this::updateNavButtonColor); + this::onDarkIntensityChanged); /** {@code 1} if the Taskbar background color is fully opaque. */ private final AnimatedFloat mOnTaskbarBackgroundNavButtonColorOverride = new AnimatedFloat( this::updateNavButtonColor); @@ -218,12 +223,19 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private ImageView mRecentsButton; private Space mSpace; + private TaskbarTransitions mTaskbarTransitions; + private @BarTransitions.TransitionMode int mTransitionMode; + + private final Runnable mAutoDim = () -> mTaskbarTransitions.setAutoDim(true); + public NavbarButtonsViewController(TaskbarActivityContext context, - @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) { + @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView, + Handler handler) { mContext = context; mNavigationBarPanelContext = navigationBarPanelContext; mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext); mNavButtonsView = navButtonsView; + mHandler = handler; mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons); mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons); mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons); @@ -233,6 +245,10 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mOnBackgroundIconColor = Utilities.isDarkTheme(context) ? context.getColor(R.color.taskbar_nav_icon_light_color) : context.getColor(R.color.taskbar_nav_icon_dark_color); + + if (mContext.isPhoneMode()) { + mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView); + } } /** @@ -247,17 +263,28 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT boolean isThreeButtonNav = mContext.isThreeButtonNav(); DeviceProfile deviceProfile = mContext.getDeviceProfile(); Resources resources = mContext.getResources(); - Point p = !mContext.isUserSetupComplete() - ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowSize()) - : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, - mContext.isPhoneMode()); - mNavButtonsView.getLayoutParams().height = p.y; + + int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize(); + Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, + mContext.isPhoneMode(), mContext.isGestureNav()); + ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams(); + navButtonsViewLayoutParams.width = p.x; + if (!mContext.isUserSetupComplete()) { + // Setup mode in phone mode uses gesture nav. + navButtonsViewLayoutParams.height = setupSize; + } else { + navButtonsViewLayoutParams.height = p.y; + } + mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams); mIsImeRenderingNavButtons = InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar(); if (!mIsImeRenderingNavButtons) { // IME switcher - mImeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH, + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : R.drawable.ic_ime_switcher; + mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH, isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, mControllers.navButtonController, R.id.ime_switcher); mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton, @@ -276,8 +303,13 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT .get(ALPHA_INDEX_SMALL_SCREEN), flags -> (flags & FLAG_SMALL_SCREEN) == 0)); - mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController - .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); + if (!mContext.isPhoneMode()) { + mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController + .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); + } + + // Start at 1 because relevant flags are unset at init. + mOnBackgroundNavButtonColorOverrideMultiplier.value = 1; // Force nav buttons (specifically back button) to be visible during setup wizard. boolean isInSetup = !mContext.isUserSetupComplete(); @@ -289,39 +321,41 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // - IME is showing (add separate translation for IME) // - VoiceInteractionWindow (assistant) is showing // - Keyboard shortcuts helper is showing - int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE - | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING; - mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, - flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, - 1, 0)); - // Center nav buttons in new height for IME. - float transForIme = (mContext.getDeviceProfile().taskbarHeight - - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f; - // For gesture nav, nav buttons only show for IME anyway so keep them translated down. - float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; - mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, - flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE, - transForIme, defaultButtonTransY)); + if (!mContext.isPhoneMode()) { + int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE + | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING; + mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, + flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, + 1, 0)); + // Center nav buttons in new height for IME. + float transForIme = (mContext.getDeviceProfile().taskbarHeight + - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f; + // For gesture nav, nav buttons only show for IME anyway so keep them translated down. + float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; + mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, + flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE, + transForIme, defaultButtonTransY)); - // Start at 1 because relevant flags are unset at init. - mOnBackgroundNavButtonColorOverrideMultiplier.value = 1; - mPropertyHolders.add(new StatePropertyHolder( - mOnBackgroundNavButtonColorOverrideMultiplier, - flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0)); + mPropertyHolders.add(new StatePropertyHolder( + mOnBackgroundNavButtonColorOverrideMultiplier, + flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0)); - mPropertyHolders.add(new StatePropertyHolder( - mSlideInViewVisibleNavButtonColorOverride, - flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); + mPropertyHolders.add(new StatePropertyHolder( + mSlideInViewVisibleNavButtonColorOverride, + flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); + } if (alwaysShowButtons) { initButtons(mNavButtonContainer, mEndContextualContainer, mControllers.navButtonController); updateButtonLayoutSpacing(); - updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode()); + updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode()); - mPropertyHolders.add(new StatePropertyHolder( - mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), - flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); + if (!mContext.isPhoneMode()) { + mPropertyHolders.add(new StatePropertyHolder( + mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), + flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); + } } else if (!mIsImeRenderingNavButtons) { View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mStartContextualContainer, mControllers.navButtonController, R.id.back); @@ -344,12 +378,15 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT R.bool.floating_rotation_button_position_left); mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton, mRotationButtonListener); + if (mContext.isPhoneMode()) { + mTaskbarTransitions.init(); + } applyState(); mPropertyHolders.forEach(StatePropertyHolder::endAnimation); // Initialize things needed to move nav buttons to separate window. - mSeparateWindowParent = new BaseDragLayer(mContext, null, 0) { + mSeparateWindowParent = new BaseDragLayer<>(mContext, null, 0) { @Override public void recreateControllers() { mControllers = new TouchController[0]; @@ -604,6 +641,48 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mBackButton.setAccessibilityDelegate(accessibilityDelegate); } + public void setWallpaperVisible(boolean isVisible) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.setWallpaperVisibility(isVisible); + } + } + + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + mTransitionMode = barMode; + if (checkBarModes) { + checkNavBarModes(); + } + } + + public void checkNavBarModes() { + if (mContext.isPhoneMode()) { + boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0; + mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden); + } + } + + public void finishBarAnimations() { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.finishAnimations(); + } + } + + public void touchAutoDim(boolean reset) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.setAutoDim(false); + mHandler.removeCallbacks(mAutoDim); + if (reset) { + mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS); + } + } + } + + public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.transitionTo(barMode, animate); + } + } + /** Use to set the translationY for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonTranslationY() { return mTaskbarNavButtonTranslationY; @@ -638,7 +717,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private void applyState() { int count = mPropertyHolders.size(); for (int i = 0; i < count; i++) { - mPropertyHolders.get(i).setState(mState); + mPropertyHolders.get(i).setState(mState, mContext.isGestureNav()); } } @@ -678,14 +757,18 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mLightIconColorOnHome, mDarkIconColorOnHome); - // Override the color from framework if nav buttons are over an opaque Taskbar surface. - final int iconColor = (int) argbEvaluator.evaluate( - mOnBackgroundNavButtonColorOverrideMultiplier.value - * Math.max( - mOnTaskbarBackgroundNavButtonColorOverride.value, - mSlideInViewVisibleNavButtonColorOverride.value), - sysUiNavButtonIconColorOnHome, - mOnBackgroundIconColor); + final int iconColor; + if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode()) { + iconColor = sysUiNavButtonIconColorOnHome; + } else { + // Override the color from framework if nav buttons are over an opaque Taskbar surface. + iconColor = (int) argbEvaluator.evaluate( + mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max( + mOnTaskbarBackgroundNavButtonColorOverride.value, + mSlideInViewVisibleNavButtonColorOverride.value), + sysUiNavButtonIconColorOnHome, + mOnBackgroundIconColor); + } for (ImageView button : mAllButtons) { button.setImageTintList(ColorStateList.valueOf(iconColor)); @@ -697,6 +780,13 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } } + private void onDarkIntensityChanged() { + updateNavButtonColor(); + if (mContext.isPhoneMode()) { + mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value); + } + } + protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) { return addButton(drawableId, buttonType, parent, navButtonController, id, @@ -1042,6 +1132,9 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT + mOnBackgroundNavButtonColorOverrideMultiplier.value); mNavButtonsView.dumpLogs(prefix + "\t", pw); + if (mContext.isPhoneMode()) { + mTaskbarTransitions.dumpLogs(prefix + "\t", pw); + } } private static String getStateString(int flags) { @@ -1078,6 +1171,62 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mHitboxExtender.onAnimationProgressToOverview(alignment); } + /** Adjusts navigation buttons layout accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + mNavButtonContainer.setTranslationX(getNavBarTranslationX(location)); + } + + /** Animates navigation buttons accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + // TODO(b/346381754) add the teleport animation similarly to the bubble bar + mNavButtonContainer.setTranslationX(getNavBarTranslationX(location)); + } + + private int getNavBarTranslationX(BubbleBarLocation location) { + boolean isNavbarOnRight = location.isOnLeft(mNavButtonsView.isLayoutRtl()); + DeviceProfile dp = mContext.getDeviceProfile(); + float navBarTargetStartX; + if (mContext.shouldStartAlignTaskbar()) { + int navBarSpacing = dp.inlineNavButtonsEndSpacingPx; + // If the taskbar is start aligned the navigation bar is aligned to the start or end of + // the container, depending on the bubble bar location + if (isNavbarOnRight) { + navBarTargetStartX = dp.widthPx - navBarSpacing - mNavButtonContainer.getWidth(); + } else { + navBarTargetStartX = navBarSpacing; + } + } else { + // If the task bar is not start aligned, the navigation bar is located in the center + // between the taskbar and screen edges, depending on the bubble bar location. + float navbarWidth = mNavButtonContainer.getWidth(); + Rect taskbarBounds = mControllers.taskbarViewController.getIconLayoutBounds(); + if (isNavbarOnRight) { + if (mNavButtonsView.isLayoutRtl()) { + float taskBarEnd = taskbarBounds.right; + navBarTargetStartX = (dp.widthPx + taskBarEnd - navbarWidth) / 2; + } else { + navBarTargetStartX = mNavButtonContainer.getLeft(); + } + } else { + float taskBarStart = taskbarBounds.left; + navBarTargetStartX = (taskBarStart - navbarWidth) / 2; + } + } + return (int) navBarTargetStartX - mNavButtonContainer.getLeft(); + } + + /** Adjusts the navigation buttons layout position according to the bubble bar location. */ + public void onTaskbarLayoutChange() { + if (com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar() + && mControllers.bubbleControllers.isPresent()) { + BubbleBarLocation bubblesLocation = mControllers.bubbleControllers.get() + .bubbleBarViewController.getBubbleBarLocation(); + onBubbleBarLocationUpdated(bubblesLocation); + } + } + private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @Override public void onVisibilityChanged(boolean isVisible) { @@ -1090,83 +1239,6 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } } - private class RotationButtonImpl implements RotationButton { - - private final ImageView mButton; - private AnimatedVectorDrawable mImageDrawable; - - RotationButtonImpl(ImageView button) { - mButton = button; - } - - @Override - public void setRotationButtonController(RotationButtonController rotationButtonController) { - // TODO(b/187754252) UI polish, different icons based on light/dark context, etc - mImageDrawable = (AnimatedVectorDrawable) mButton.getContext() - .getDrawable(rotationButtonController.getIconResId()); - mButton.setImageDrawable(mImageDrawable); - mButton.setContentDescription(mButton.getResources() - .getString(R.string.accessibility_rotate_button)); - mImageDrawable.setCallback(mButton); - } - - @Override - public View getCurrentView() { - return mButton; - } - - @Override - public boolean show() { - mButton.setVisibility(View.VISIBLE); - mState |= FLAG_ROTATION_BUTTON_VISIBLE; - applyState(); - return true; - } - - @Override - public boolean hide() { - mButton.setVisibility(View.GONE); - mState &= ~FLAG_ROTATION_BUTTON_VISIBLE; - applyState(); - return true; - } - - @Override - public boolean isVisible() { - return mButton.getVisibility() == View.VISIBLE; - } - - @Override - public void updateIcon(int lightIconColor, int darkIconColor) { - // TODO(b/187754252): UI Polish - } - - @Override - public void setOnClickListener(OnClickListener onClickListener) { - mButton.setOnClickListener(onClickListener); - } - - @Override - public void setOnHoverListener(OnHoverListener onHoverListener) { - mButton.setOnHoverListener(onHoverListener); - } - - @Override - public AnimatedVectorDrawable getImageDrawable() { - return mImageDrawable; - } - - @Override - public void setDarkIntensity(float darkIntensity) { - // TODO(b/187754252) UI polish - } - - @Override - public boolean acceptRotationProposal() { - return mButton.isAttachedToWindow(); - } - } - private static class StatePropertyHolder { private final float mEnabledValue, mDisabledValue; @@ -1197,13 +1269,16 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue); } - public void setState(int flags) { + public void setState(int flags, boolean skipAnimation) { boolean isEnabled = mEnableCondition.test(flags); if (mIsEnabled != isEnabled) { mIsEnabled = isEnabled; mAnimator.cancel(); mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue); mAnimator.start(); + if (skipAnimation) { + mAnimator.end(); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt new file mode 100644 index 0000000000..dc66e0b7ca --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.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 + +import android.content.Context +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.view.View +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.views.ActivityContext + +/** + * A single menu item shortcut to execute creating a new instance of an app. Default interaction for + * [onClick] is to launch the app in full screen or as a floating window in Desktop Mode. + */ +class NewWindowTaskbarShortcut(target: T, itemInfo: ItemInfo?, originalView: View?) : + SystemShortcut( + R.drawable.desktop_mode_ic_taskbar_menu_new_window, + R.string.new_window_option_taskbar, + target, + itemInfo, + originalView + ) where T : Context?, T : ActivityContext? { + + override fun onClick(v: View?) { + val intent = mItemInfo.intent ?: return + intent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + mTarget?.startActivitySafely(v, intent, mItemInfo) + AbstractFloatingView.closeAllOpenViews(mTarget) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java index 94e2244172..caf3320ea6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java @@ -47,6 +47,7 @@ public class StashedHandleView extends View { private final int[] mTmpArr = new int[2]; private @Nullable ObjectAnimator mColorChangeAnim; + private Boolean mIsRegionDark; public StashedHandleView(Context context) { this(context, null); @@ -95,7 +96,11 @@ public class StashedHandleView extends View { * @param animate Whether to animate the change, or apply it immediately. */ public void updateHandleColor(boolean isRegionDark, boolean animate) { + if (mIsRegionDark != null && mIsRegionDark == isRegionDark) { + return; + } int newColor = isRegionDark ? mStashedHandleLightColor : mStashedHandleDarkColor; + mIsRegionDark = isRegionDark; if (mColorChangeAnim != null) { mColorChangeAnim.cancel(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java index 252f2a81f0..7273facbae 100644 --- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java @@ -41,8 +41,8 @@ import com.android.launcher3.util.Executors; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.NavHandle; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import java.io.PrintWriter; @@ -94,6 +94,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT // States that affect whether region sampling is enabled or not private boolean mIsStashed; private boolean mIsLumaSamplingEnabled; + private boolean mIsAppTransitionPending; private boolean mTaskbarHidden; private float mTranslationYForSwipe; @@ -209,7 +210,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT * morphs into the size of where the taskbar icons will be. */ public Animator createRevealAnimToIsStashed(boolean isStashed) { - Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds()); + Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds(); float startRadius = mStashedHandleRadius; if (DisplayController.isTransientTaskbar(mActivity)) { @@ -257,6 +258,11 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT updateSamplingState(); } + public void setIsAppTransitionPending(boolean pending) { + mIsAppTransitionPending = pending; + updateSamplingState(); + } + private void updateSamplingState() { updateRegionSamplingWindowVisibility(); if (shouldSample()) { @@ -268,7 +274,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT } private boolean shouldSample() { - return mIsStashed && mIsLumaSamplingEnabled; + return mIsStashed && mIsLumaSamplingEnabled && !mIsAppTransitionPending; } protected void updateStashedHandleHintScale() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 6b62c86792..c355e4690d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -25,9 +25,11 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY; import static com.android.launcher3.Flags.enableCursorHoverStates; +import static com.android.launcher3.Flags.taskbarOverflow; import static com.android.launcher3.Utilities.calculateTextHeight; import static com.android.launcher3.Utilities.isRunningInTestHarness; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; @@ -36,6 +38,7 @@ import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN; +import static com.android.launcher3.taskbar.TaskbarStashController.SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE; import static com.android.launcher3.testing.shared.ResourceUtils.getBoolByName; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; @@ -43,6 +46,8 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import static com.android.wm.shell.Flags.enableTinyTaskbar; +import static java.lang.invoke.MethodHandles.Lookup.PROTECTED; + import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.app.ActivityOptions; @@ -67,6 +72,7 @@ import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Toast; +import android.window.RemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -92,9 +98,11 @@ 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.TaskItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag; import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; @@ -103,11 +111,18 @@ import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; import com.android.launcher3.taskbar.bubbles.BubbleBarView; 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.BubbleStashController; import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider; +import com.android.launcher3.taskbar.bubbles.stashing.DeviceProfileDimensionsProviderAdapter; +import com.android.launcher3.taskbar.bubbles.stashing.PersistentBubbleStashController; +import com.android.launcher3.taskbar.bubbles.stashing.TransientBubbleStashController; +import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator; +import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator; import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; import com.android.launcher3.testing.TestLogging; @@ -128,17 +143,22 @@ import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.ViewCache; import com.android.launcher3.views.ActivityContext; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsModel; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.systemui.unfold.updates.RotationChangeProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; +import com.android.wm.shell.Flags; import java.io.PrintWriter; import java.util.Collections; @@ -167,6 +187,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { private final WindowManager mWindowManager; private DeviceProfile mDeviceProfile; private WindowManager.LayoutParams mWindowLayoutParams; + private WindowManager.LayoutParams mLastUpdatedLayoutParams; private boolean mIsFullscreen; // The size we should return to when we call setTaskbarWindowFullscreen(false) private int mLastRequestedNonFullscreenSize; @@ -197,15 +218,26 @@ public class TaskbarActivityContext extends BaseTaskbarContext { private final LauncherPrefs mLauncherPrefs; + private TaskbarFeatureEvaluator mTaskbarFeatureEvaluator; + + private TaskbarSpecsEvaluator mTaskbarSpecsEvaluator; + public TaskbarActivityContext(Context windowContext, @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp, TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider - unfoldTransitionProgressProvider) { + unfoldTransitionProgressProvider, + @NonNull DesktopVisibilityController desktopVisibilityController) { super(windowContext); mNavigationBarPanelContext = navigationBarPanelContext; applyDeviceProfile(launcherDp); final Resources resources = getResources(); + mTaskbarFeatureEvaluator = TaskbarFeatureEvaluator.getInstance(this); + mTaskbarSpecsEvaluator = new TaskbarSpecsEvaluator( + this, + mTaskbarFeatureEvaluator, + mDeviceProfile.inv.numRows, + mDeviceProfile.inv.numColumns); mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false); mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode", @@ -226,15 +258,18 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mWindowManager = c.getSystemService(WindowManager.class); // Inflate views. - int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode() - ? R.layout.transient_taskbar - : R.layout.taskbar; + final boolean isTransientTaskbar = DisplayController.isTransientTaskbar(this) + && !isPhoneMode(); + int taskbarLayout = isTransientTaskbar ? R.layout.transient_taskbar : R.layout.taskbar; mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false); TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view); TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim); NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view); StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle); - BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles); + BubbleBarView bubbleBarView = null; + if (isTransientTaskbar || Flags.enableBubbleBarInPersistentTaskBar()) { + bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles); + } StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle); mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this); @@ -243,17 +278,28 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Optional bubbleControllersOptional = Optional.empty(); BubbleBarController.onTaskbarRecreated(); if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) { + Optional bubbleHandleController = Optional.empty(); + if (isTransientTaskbar) { + bubbleHandleController = Optional.of( + new BubbleStashedHandleViewController(this, bubbleHandleView)); + } + TaskbarHotseatDimensionsProvider dimensionsProvider = + new DeviceProfileDimensionsProviderAdapter(this); + BubbleStashController bubbleStashController = isTransientTaskbar + ? new TransientBubbleStashController(dimensionsProvider, this) + : new PersistentBubbleStashController(dimensionsProvider); bubbleControllersOptional = Optional.of(new BubbleControllers( new BubbleBarController(this, bubbleBarView), new BubbleBarViewController(this, bubbleBarView), - new BubbleStashController(this), - new BubbleStashedHandleViewController(this, bubbleHandleView), + bubbleStashController, + bubbleHandleController, new BubbleDragController(this), new BubbleDismissController(this, mDragLayer), new BubbleBarPinController(this, mDragLayer, () -> DisplayController.INSTANCE.get(this).getInfo().currentSize), new BubblePinController(this, mDragLayer, - () -> DisplayController.INSTANCE.get(this).getInfo().currentSize) + () -> DisplayController.INSTANCE.get(this).getInfo().currentSize), + new BubbleCreator(this) )); } @@ -271,7 +317,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers = new TaskbarControllers(this, new TaskbarDragController(this), buttonController, - new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView), + new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView, + getMainThreadHandler()), rotationButtonController, new TaskbarDragLayerController(this, mDragLayer), new TaskbarViewController(this, taskbarView), @@ -292,14 +339,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { new VoiceInteractionWindowController(this), new TaskbarTranslationController(this), new TaskbarSpringOnStashController(this), - new TaskbarRecentAppsController( - RecentsModel.INSTANCE.get(this), - LauncherActivityInterface.INSTANCE::getDesktopVisibilityController), + new TaskbarRecentAppsController(this, RecentsModel.INSTANCE.get(this)), TaskbarEduTooltipController.newInstance(this), new KeyboardQuickSwitchController(), - new TaskbarPinningController(this, () -> - DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()), - bubbleControllersOptional); + new TaskbarPinningController(this), + bubbleControllersOptional, + new TaskbarDesktopModeController(desktopVisibilityController)); mLauncherPrefs = LauncherPrefs.get(this); } @@ -362,6 +407,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false); mLastRequestedNonFullscreenSize = getDefaultTaskbarWindowSize(); mWindowLayoutParams = createAllWindowParams(); + mLastUpdatedLayoutParams = new WindowManager.LayoutParams(); // Initialize controllers after all are constructed. mControllers.init(sharedState); @@ -421,6 +467,15 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return enableTinyTaskbar() && mDeviceProfile.isPhone && mDeviceProfile.isTaskbarPresent; } + /** + * Returns {@code true} iff bubble bar is enabled (but not necessarily visible / + * containing bubbles). + */ + @Override + public boolean isBubbleBarEnabled() { + return getBubbleControllers() != null && BubbleBarController.isBubbleBarEnabled(); + } + /** * Returns if software keyboard is docked or input toolbar is placed at the taskbar area */ @@ -447,7 +502,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * Show Taskbar upon receiving broadcast */ public void showTaskbarFromBroadcast() { - mControllers.taskbarStashController.showTaskbarFromBroadcast(); + // If user is in middle of taskbar education handle go to next step of education + if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) { + mControllers.taskbarEduTooltipController.hide(); + mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); + } + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false); } /** Toggles Taskbar All Apps overlay. */ @@ -597,6 +657,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mNavMode == NavigationMode.THREE_BUTTONS; } + /** Returns whether taskbar should start align. */ + public boolean shouldStartAlignTaskbar() { + return isThreeButtonNav() && mDeviceProfile.startAlignTaskbar; + } + public boolean isGestureNav() { return mNavMode == NavigationMode.NO_BUTTON; } @@ -786,6 +851,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { */ public void setUIController(@NonNull TaskbarUIController uiController) { mControllers.setUiController(uiController); + if (BubbleBarController.isBubbleBarEnabled() && mControllers.bubbleControllers.isEmpty()) { + // if the bubble bar was visible in a previous configuration of taskbar and is being + // recreated now without bubbles, clean up any bubble bar adjustments from hotseat + bubbleBarVisibilityChanged(/* isVisible= */ false); + } } /** @@ -795,11 +865,37 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.taskbarStashController.setSetupUIVisible(isVisible); } + public void setWallpaperVisible(boolean isVisible) { + mControllers.navbarButtonsViewController.setWallpaperVisible(isVisible); + } + + public void checkNavBarModes() { + mControllers.navbarButtonsViewController.checkNavBarModes(); + } + + public void finishBarAnimations() { + mControllers.navbarButtonsViewController.finishBarAnimations(); + } + + public void touchAutoDim(boolean reset) { + mControllers.navbarButtonsViewController.touchAutoDim(reset); + } + + public void transitionTo(@BarTransitions.TransitionMode int barMode, + boolean animate) { + mControllers.navbarButtonsViewController.transitionTo(barMode, animate); + } + + public void appTransitionPending(boolean pending) { + mControllers.stashedHandleViewController.setIsAppTransitionPending(pending); + } + /** * Called when this instance of taskbar is no longer needed */ public void onDestroy() { mIsDestroyed = true; + mTaskbarFeatureEvaluator.onDestroy(); setUIController(TaskbarUIController.DEFAULT); mControllers.onDestroy(); if (!enableTaskbarNoRecreate() && !ENABLE_TASKBAR_NAVBAR_UNIFICATION) { @@ -817,7 +913,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0; - onNotificationShadeExpandChanged(isShadeVisible, fromInit); + onNotificationShadeExpandChanged(isShadeVisible, fromInit || isPhoneMode()); mControllers.taskbarViewController.setRecentsButtonDisabled( mControllers.navbarButtonsViewController.isRecentsDisabled() || isNavBarKidsModeActive()); @@ -836,8 +932,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags); mControllers.bubbleControllers.ifPresent(controllers -> { controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags); - controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled( - mControllers.navbarButtonsViewController.isHomeDisabled()); + controllers.bubbleStashedHandleViewController.ifPresent(controller -> + controller.setIsHomeButtonDisabled( + mControllers.navbarButtonsViewController.isHomeDisabled())); }); } @@ -872,6 +969,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.rotationButtonController.onBehaviorChanged(displayId, behavior); } + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + mControllers.navbarButtonsViewController.onTransitionModeUpdated(barMode, checkBarModes); + } + public void onNavButtonsDarkIntensityChanged(float darkIntensity) { mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity() .updateValue(darkIntensity); @@ -1021,6 +1122,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * window. */ public void setTaskbarWindowFocusable(boolean focusable) { + if (isPhoneMode()) { + return; + } if (focusable) { mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; } else { @@ -1033,7 +1137,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed. */ public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) { - if (!DisplayController.isTransientTaskbar(this)) { + if (!DisplayController.isTransientTaskbar(this) || isPhoneMode()) { return; } if (shouldForceShow) { @@ -1076,15 +1180,26 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.startSplitSelection(splitSelectSource); } + boolean areDesktopTasksVisible() { + return mControllers != null + && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible(); + } + protected void onTaskbarIconClicked(View view) { TaskbarUIController taskbarUIController = mControllers.uiController; RecentsView recents = taskbarUIController.getRecentsView(); boolean shouldCloseAllOpenViews = true; Object tag = view.getTag(); - if (tag instanceof Task) { - Task task = (Task) tag; - ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key, - ActivityOptions.makeBasic()); + + if (taskbarOverflow()) { + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false); + } + + if (tag instanceof GroupTask groupTask) { + handleGroupTaskLaunch( + groupTask, + /* remoteTransition= */ null, + areDesktopTasksVisible()); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } else if (tag instanceof FolderInfo) { // Tapping an expandable folder icon on Taskbar @@ -1101,6 +1216,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.onTaskbarIconLaunched(api); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } + } else if (tag instanceof TaskItemInfo info) { + UI_HELPER_EXECUTOR.execute(() -> + SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId())); + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar( + /* stash= */ true); } else if (tag instanceof WorkspaceItemInfo) { // Tapping a launchable icon on Taskbar WorkspaceItemInfo info = (WorkspaceItemInfo) tag; @@ -1184,6 +1304,58 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } + public void handleGroupTaskLaunch( + GroupTask task, + @Nullable RemoteTransition remoteTransition, + boolean onDesktop) { + handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null); + } + + /** + * Launches the given GroupTask with the following behavior: + * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop. + * - If {@code onDesktop}, bring the given GroupTask to the front. + * - If the GroupTask is a single task, launch it via startActivityFromRecents. + * - Otherwise, we assume the GroupTask is a Split pair and launch them together. + *

+ * Given start and/or finish callbacks, they will be run before an after the app launch + * respectively in cases where we can't use the remote transition, otherwise we will assume that + * these callbacks are included in the remote transition. + */ + public void handleGroupTaskLaunch( + GroupTask task, + @Nullable RemoteTransition remoteTransition, + boolean onDesktop, + @Nullable Runnable onStartCallback, + @Nullable Runnable onFinishCallback) { + if (task instanceof DesktopTask) { + UI_HELPER_EXECUTOR.execute(() -> + SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(), + remoteTransition)); + } else if (onDesktop) { + UI_HELPER_EXECUTOR.execute(() -> { + if (onStartCallback != null) { + onStartCallback.run(); + } + SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id); + if (onFinishCallback != null) { + onFinishCallback.run(); + } + }); + } else if (task.task2 == null) { + UI_HELPER_EXECUTOR.execute(() -> { + ActivityOptions activityOptions = + makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options; + activityOptions.setRemoteTransition(remoteTransition); + + ActivityManagerWrapper.getInstance().startActivityFromRecents( + task.task1.key, activityOptions); + }); + } else { + mControllers.uiController.launchSplitTasks(task, remoteTransition); + } + } + /** * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app), * and calls the appropriate method to animate and launch. @@ -1244,10 +1416,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { if (foundTask != null) { TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); if (foundTaskView != null - && foundTaskView.isVisibleToUser()) { + && foundTaskView.isVisibleToUser() + && !(foundTaskView instanceof DesktopTaskView)) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon"); - foundTaskView.launchTasks(); + foundTaskView.launchWithAnimation(); return; } } @@ -1295,7 +1468,6 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return; } } - startActivity(intent); } else { getSystemService(LauncherApps.class).startMainActivity( @@ -1340,6 +1512,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { itemView.setHapticFeedbackEnabled(true); return false; }); + + // Close any open taskbar tooltips. + if (AbstractFloatingView.hasOpenView(this, TYPE_ON_BOARD_POPUP)) { + AbstractFloatingView.getOpenView(this, TYPE_ON_BOARD_POPUP) + .close(/* animate= */ false); + } }); } @@ -1352,10 +1530,14 @@ public class TaskbarActivityContext extends BaseTaskbarContext { /** * Called when we want to unstash taskbar when user performs swipes up gesture. + * @param delayTaskbarBackground whether we will delay the taskbar background animation */ - public void onSwipeToUnstashTaskbar() { + public void onSwipeToUnstashTaskbar(boolean delayTaskbarBackground) { + mControllers.uiController.onSwipeToUnstashTaskbar(); + boolean wasStashed = mControllers.taskbarStashController.isStashed(); - mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false); + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false, + SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, delayTaskbarBackground); boolean isStashed = mControllers.taskbarStashController.isStashed(); if (isStashed != wasStashed) { VibratorWrapper.INSTANCE.get(this).vibrateForTaskbarUnstash(); @@ -1456,7 +1638,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mIsNavBarKidsMode && isThreeButtonNav(); } - protected boolean isNavBarForceVisible() { + @VisibleForTesting(otherwise = PROTECTED) + public boolean isNavBarForceVisible() { return mIsNavBarForceVisible; } @@ -1488,7 +1671,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { duration); View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView(); - if (allAppsButton != null && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) { + if (allAppsButton != null && !FeatureFlags.enableAllAppsButtonInHotseat()) { ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1); alphaOverride.setDuration(duration); alphaOverride.addUpdateListener(a -> { @@ -1507,7 +1690,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * @param exclude {@code true} then the magnification region computation will omit the window. */ public void excludeFromMagnificationRegion(boolean exclude) { - if (mIsExcludeFromMagnificationRegion == exclude) { + if (mIsExcludeFromMagnificationRegion == exclude || isPhoneMode()) { return; } @@ -1524,6 +1707,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { void notifyUpdateLayoutParams() { if (mDragLayer.isAttachedToWindow()) { + // Copy the current windowLayoutParams to mLastUpdatedLayoutParams and compare the diff. + // If there is no change, we will skip the call to updateViewLayout. + int changes = mLastUpdatedLayoutParams.copyFrom(mWindowLayoutParams); + if (changes == 0) { + return; + } if (enableTaskbarNoRecreate()) { mWindowManager.updateViewLayout(mDragLayer.getRootView(), mWindowLayoutParams); } else { @@ -1549,6 +1738,14 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.taskbarStashController.isInStashedLauncherState(); } + public TaskbarFeatureEvaluator getTaskbarFeatureEvaluator() { + return mTaskbarFeatureEvaluator; + } + + public TaskbarSpecsEvaluator getTaskbarSpecsEvaluator() { + return mTaskbarSpecsEvaluator; + } + protected void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarActivityContext:"); @@ -1590,6 +1787,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.uiController.canToggleHomeAllApps(); } + boolean isIconAlignedWithHotseat() { + return mControllers.uiController.isIconAlignedWithHotseat(); + } + @VisibleForTesting public TaskbarControllers getControllers() { return mControllers; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt index 2737cbd467..c0e921edcf 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt @@ -23,6 +23,7 @@ import android.graphics.Paint import android.graphics.Path import android.graphics.RectF import com.android.app.animation.Interpolators +import com.android.internal.policy.ScreenDecorationsUtils import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.Utilities.mapRange @@ -66,8 +67,8 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { private var keyShadowDistance = 0f private var bottomMargin = 0 - private val fullCornerRadius = context.cornerRadius.toFloat() - private var cornerRadius = fullCornerRadius + private val fullCornerRadius: Float + private var cornerRadius = 0f private var widthInsetPercentage = 0f private val square = Path() private val circle = Path() @@ -97,7 +98,14 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { shadowAlpha = LIGHT_THEME_SHADOW_ALPHA } - setCornerRoundness(DEFAULT_ROUNDNESS) + if (context.areDesktopTasksVisible()) { + fullCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + cornerRadius = fullCornerRadius + } else { + fullCornerRadius = context.cornerRadius.toFloat() + cornerRadius = fullCornerRadius + setCornerRoundness(MAX_ROUNDNESS) + } } fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) { @@ -188,7 +196,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { mapRange( scale, 0f, - res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat() + res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat(), ) .toInt() shadowBlur = @@ -227,7 +235,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { -mapRange( 1f - progress, 0f, - if (isAnimatingPinning) 0f else stashedHandleHeight / 2f + if (isAnimatingPinning) 0f else stashedHandleHeight / 2f, ) // Draw shadow. @@ -237,7 +245,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { shadowBlur, 0f, keyShadowDistance, - setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)) + setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)), ) strokePaint.alpha = (paint.alpha * strokeAlpha) / 255 @@ -263,7 +271,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { } companion object { - const val DEFAULT_ROUNDNESS = 1f + const val MAX_ROUNDNESS = 1f private const val DARK_THEME_STROKE_ALPHA = 51 private const val LIGHT_THEME_STROKE_ALPHA = 41 private const val DARK_THEME_SHADOW_ALPHA = 51f diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index 58c5e835c9..56fd2bb66d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -64,6 +64,7 @@ public class TaskbarControllers { public final KeyboardQuickSwitchController keyboardQuickSwitchController; public final TaskbarPinningController taskbarPinningController; public final Optional bubbleControllers; + public final TaskbarDesktopModeController taskbarDesktopModeController; @Nullable private LoggableTaskbarController[] mControllersToLog = null; @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null; @@ -111,7 +112,8 @@ public class TaskbarControllers { TaskbarEduTooltipController taskbarEduTooltipController, KeyboardQuickSwitchController keyboardQuickSwitchController, TaskbarPinningController taskbarPinningController, - Optional bubbleControllers) { + Optional bubbleControllers, + TaskbarDesktopModeController taskbarDesktopModeController) { this.taskbarActivityContext = taskbarActivityContext; this.taskbarDragController = taskbarDragController; this.navButtonController = navButtonController; @@ -138,6 +140,7 @@ public class TaskbarControllers { this.keyboardQuickSwitchController = keyboardQuickSwitchController; this.taskbarPinningController = taskbarPinningController; this.bubbleControllers = bubbleControllers; + this.taskbarDesktopModeController = taskbarDesktopModeController; } /** @@ -165,6 +168,7 @@ public class TaskbarControllers { taskbarOverlayController.init(this); taskbarAllAppsController.init(this, sharedState.allAppsVisible); navButtonController.init(this); + bubbleControllers.ifPresent(controllers -> controllers.init(this)); taskbarInsetsController.init(this); voiceInteractionWindowController.init(this); taskbarRecentAppsController.init(this); @@ -172,7 +176,7 @@ public class TaskbarControllers { taskbarEduTooltipController.init(this); keyboardQuickSwitchController.init(this); taskbarPinningController.init(this, mSharedState); - bubbleControllers.ifPresent(controllers -> controllers.init(this)); + taskbarDesktopModeController.init(this, mSharedState); mControllersToLog = new LoggableTaskbarController[] { taskbarDragController, navButtonController, navbarButtonsViewController, @@ -188,8 +192,18 @@ public class TaskbarControllers { taskbarDragLayerController, taskbarScrimViewController, voiceInteractionWindowController }; - mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS); + if (taskbarDesktopModeController.getAreDesktopTasksVisible()) { + mCornerRoundness.updateValue(taskbarDesktopModeController.getTaskbarCornerRoundness( + mSharedState.showCornerRadiusInDesktopMode)); + } else { + mCornerRoundness.updateValue(TaskbarBackgroundRenderer.MAX_ROUNDNESS); + } + onPostInit(); + } + + @VisibleForTesting + public void onPostInit() { mAreAllControllersInitialized = true; for (Runnable postInitCallback : mPostInitCallbacks) { postInitCallback.run(); @@ -248,6 +262,7 @@ public class TaskbarControllers { keyboardQuickSwitchController.onDestroy(); taskbarStashController.onDestroy(); bubbleControllers.ifPresent(controllers -> controllers.onDestroy()); + taskbarDesktopModeController.onDestroy(); mControllersToLog = null; mBackgroundRendererControllers = null; @@ -282,6 +297,11 @@ public class TaskbarControllers { } uiController.dumpLogs(prefix + "\t", pw); rotationButtonController.dumpLogs(prefix + "\t", pw); + if (bubbleControllers.isPresent()) { + bubbleControllers.get().dump(pw); + } else { + pw.println(String.format("%s\t%s", prefix, "Bubble controllers are empty.")); + } } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt new file mode 100644 index 0000000000..47a35c55ab --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt @@ -0,0 +1,54 @@ +/* + * 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.statehandlers.DesktopVisibilityController +import com.android.launcher3.statehandlers.DesktopVisibilityController.TaskbarDesktopModeListener +import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS + +/** Handles Taskbar in Desktop Windowing mode. */ +class TaskbarDesktopModeController( + private val desktopVisibilityController: DesktopVisibilityController +) : TaskbarDesktopModeListener { + private lateinit var taskbarControllers: TaskbarControllers + private lateinit var taskbarSharedState: TaskbarSharedState + + val areDesktopTasksVisible: Boolean + get() = desktopVisibilityController.areDesktopTasksVisible() + + fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) { + taskbarControllers = controllers + taskbarSharedState = sharedState + desktopVisibilityController.registerTaskbarDesktopModeListener(this) + } + + override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) { + taskbarSharedState.showCornerRadiusInDesktopMode = doesAnyTaskRequireTaskbarRounding + val cornerRadius = getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding) + taskbarControllers.taskbarCornerRoundness.animateToValue(cornerRadius).start() + } + + fun getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding: Boolean): Float { + return if (doesAnyTaskRequireTaskbarRounding) { + MAX_ROUNDNESS + } else { + 0f + } + } + + fun onDestroy() = desktopVisibilityController.unregisterTaskbarDesktopModeListener(this) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt index a635537907..b5a33147ce 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt @@ -169,8 +169,11 @@ constructor( override fun addArrow() { super.addArrow() + val location = IntArray(2) + popupContainer.getLocationInDragLayer(dividerView, location) + val dividerViewX = location[0].toFloat() // Change arrow location to the middle of popup. - mArrow.x = (dividerView.x + dividerView.width / 2) - (mArrowWidth / 2) + mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2) } override fun updateArrowColor() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index 4f5922c29f..fc307b2e88 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -74,18 +74,17 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.views.BubbleTextHolder; -import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LogUtils; import com.android.quickstep.util.MultiValueUpdateListener; import com.android.systemui.shared.recents.model.Task; -import com.android.wm.shell.draganddrop.DragAndDropConstants; +import com.android.wm.shell.shared.draganddrop.DragAndDropConstants; import java.io.PrintWriter; import java.util.Arrays; @@ -181,7 +180,9 @@ public class TaskbarDragController extends DragController im private DragView startInternalDrag( BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) { - float iconScale = btv.getIcon().getAnimatedScale(); + // TODO(b/344038728): null check is only necessary because Recents doesn't use + // FastBitmapDrawable + float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale(); // Clear the pressed state if necessary btv.clearFocus(); @@ -248,7 +249,7 @@ public class TaskbarDragController extends DragController im dragLayerX + dragOffset.x, dragLayerY + dragOffset.y, (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */, - (ItemInfo) btv.getTag(), + btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null, dragRect, scale * iconScale, scale, @@ -288,7 +289,9 @@ public class TaskbarDragController extends DragController im initialDragViewScale, dragViewScaleOnDrop, scalePx); - dragView.setItemInfo(dragInfo); + if (dragInfo != null) { + dragView.setItemInfo(dragInfo); + } mDragObject.dragComplete = false; mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft); @@ -301,7 +304,8 @@ public class TaskbarDragController extends DragController im mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; - mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy(); + mDragObject.originalDragInfo = + mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null; if (mOptions.preDragCondition != null) { dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0 @@ -338,12 +342,9 @@ public class TaskbarDragController extends DragController im protected void callOnDragStart() { super.callOnDragStart(); // TODO(297921594) clean it up when taskbar to desktop drag is implemented. - DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - // Pre-drag has ended, start the global system drag. - if (mDisallowGlobalDrag || (desktopController != null - && desktopController.areDesktopTasksVisible())) { + if (mDisallowGlobalDrag + || mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) { AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS); return; } @@ -431,8 +432,8 @@ public class TaskbarDragController extends DragController im null, item.user)); } intent.putExtra(Intent.EXTRA_USER, item.user); - } else if (tag instanceof Task) { - Task task = (Task) tag; + } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) { + Task task = groupTask.task1; clipDescription = new ClipDescription(task.titleDescription, new String[] { ClipDescription.MIMETYPE_APPLICATION_TASK diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java index f703463117..a9b34d2cc0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java @@ -18,7 +18,6 @@ package com.android.launcher3.taskbar; import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.KEYCODE_BACK; -import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import android.content.Context; @@ -42,7 +41,6 @@ import com.android.app.viewcapture.ViewCaptureFactory; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.views.BaseDragLayer; @@ -106,10 +104,6 @@ public class TaskbarDragLayer extends BaseDragLayer { mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT, (a, b) -> a * b, 1f); mTaskbarBackgroundAlpha.get(INDEX_ALL_OTHER_STATES).setValue(0); - mTaskbarBackgroundAlpha.get(INDEX_STASH_ANIM).setValue( - enableScalingRevealHomeAnimation() && DisplayController.isTransientTaskbar(context) - ? 0 - : 1); } public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java index ff890fb18e..2845ceee7a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java @@ -171,6 +171,10 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa } private void updateBackgroundAlpha() { + if (mActivity.isPhoneMode()) { + return; + } + final float bgNavbar = mBgNavbar.value; final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value @@ -291,7 +295,7 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa if (mActivity.isPhoneMode()) { Resources resources = mActivity.getResources(); Point taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, - resources, true /* isPhoneMode */); + resources, true /* isPhoneMode */, mActivity.isGestureNav()); return taskbarDimensions.y == -1 ? deviceProfile.getDisplayInfo().currentSize.y : taskbarDimensions.y; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt index 7f9d8a390e..19e987234f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt @@ -53,8 +53,7 @@ constructor( private val activityContext: ActivityContext = ActivityContext.lookupContext(context) - private val backgroundColor = - Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright) + private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright) private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index d57c4838d7..06376d3fd2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -33,6 +33,7 @@ import android.view.accessibility.AccessibilityNodeInfo import android.widget.TextView import androidx.annotation.IntDef import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import androidx.core.text.HtmlCompat import androidx.core.view.updateLayoutParams import com.airbnb.lottie.LottieAnimationView @@ -87,7 +88,7 @@ open class TaskbarEduTooltipController(context: Context) : !activityContext.isTinyTaskbar } - private val isOpen: Boolean + val isTooltipOpen: Boolean get() = tooltip?.isOpen ?: false val isBeforeTooltipFeaturesStep: Boolean @@ -96,7 +97,8 @@ open class TaskbarEduTooltipController(context: Context) : private lateinit var controllers: TaskbarControllers // Keep track of whether the user has seen the Search Edu - private var userHasSeenSearchEdu: Boolean + @VisibleForTesting + var userHasSeenSearchEdu: Boolean get() { return TASKBAR_SEARCH_EDU_SEEN.get(activityContext) } @@ -409,7 +411,7 @@ open class TaskbarEduTooltipController(context: Context) : override fun dumpLogs(prefix: String?, pw: PrintWriter?) { pw?.println(prefix + "TaskbarEduTooltipController:") pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled") - pw?.println("$prefix\tisOpen=$isOpen") + pw?.println("$prefix\tisOpen=$isTooltipOpen") pw?.println("$prefix\ttooltipStep=$tooltipStep") } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java index 6ac862e9e0..8a86402e7b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java @@ -85,6 +85,9 @@ public class TaskbarForceVisibleImmersiveController implements TouchController { /** Update values tracked via sysui flags. */ public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) { + if (mContext.isPhoneMode()) { + return; + } mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0; if (mContext.isNavBarForceVisible()) { if (mIsImmersiveMode) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java index 044319796e..3bff31f802 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java @@ -18,29 +18,22 @@ package com.android.launcher3.taskbar; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; import static android.view.View.ALPHA; -import static android.view.View.SCALE_Y; -import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP; +import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS; -import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; import android.view.ContextThemeWrapper; import android.view.MotionEvent; import android.view.View; -import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.views.ArrowTipView; @@ -48,19 +41,16 @@ import com.android.launcher3.views.ArrowTipView; * Controls showing a tooltip in the taskbar above each icon when it is hovered. */ public class TaskbarHoverToolTipController implements View.OnHoverListener { - - private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 250; - private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150; - - private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper()); - private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip; - private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip; + // Short duration to reveal tooltip, as it is positioned in the x/y via a post() call in + // parallel with the open animation. An instant animation could show in the wrong location. + private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 15; private final TaskbarActivityContext mActivity; private final TaskbarView mTaskbarView; private final View mHoverView; private final ArrowTipView mHoverToolTipView; private final String mToolTipText; + private final int mYOffset; public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView, View hoverView) { @@ -73,6 +63,8 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { } else if (mHoverView instanceof FolderIcon && ((FolderIcon) mHoverView).mInfo.title != null) { mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString(); + } else if (mHoverView instanceof AppPairIcon) { + mToolTipText = ((AppPairIcon) mHoverView).getTitleTextView().getText().toString(); } else { mToolTipText = null; } @@ -87,90 +79,51 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { R.dimen.taskbar_tooltip_horizontal_padding); mHoverToolTipView.findViewById(R.id.text).setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); - - AnimatorSet hoverCloseAnimator = new AnimatorSet(); - ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0); - textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f)); - ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0); - alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f)); - ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0); - scaleCloseAnimator.setInterpolator(Interpolators.STANDARD); - hoverCloseAnimator.playTogether( - textCloseAnimator, - alphaCloseAnimator, - scaleCloseAnimator); - hoverCloseAnimator.setStartDelay(0); - hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION); - mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator); + mHoverToolTipView.setAlpha(0); + mYOffset = arrowContextWrapper.getResources().getDimensionPixelSize( + R.dimen.taskbar_tooltip_y_offset); AnimatorSet hoverOpenAnimator = new AnimatorSet(); - ObjectAnimator textOpenAnimator = - ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0, 255); - textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.15f, 0.75f)); - ObjectAnimator scaleOpenAnimator = - ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0f, 1f); - scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED); ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0f, 1f); - alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, 0.33f)); - hoverOpenAnimator.playTogether( - scaleOpenAnimator, - textOpenAnimator, - alphaOpenAnimator); + hoverOpenAnimator.play(alphaOpenAnimator); hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION); mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator); mHoverToolTipView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { mHoverToolTipView.setPivotY(bottom); - mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top)); + mHoverToolTipView.setY(mTaskbarView.getTop() - mYOffset - (bottom - top)); }); } @Override public boolean onHover(View v, MotionEvent event) { - boolean isAnyOtherFloatingViewOpen = - AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP); - if (isAnyOtherFloatingViewOpen) { - mHoverToolTipHandler.removeCallbacksAndMessages(null); - } + boolean isFolderOpen = AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER); // If hover leaves a taskbar icon animate the tooltip closed. if (event.getAction() == ACTION_HOVER_EXIT) { - startHideHoverToolTip(); + mHoverToolTipView.close(/* animate= */ false); mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false); - return true; - } else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) { - // If hovering above a taskbar icon starts, animate the tooltip open. Do not - // reveal if any floating views such as folders or edu pop-ups are open. - startRevealHoverToolTip(); + } else if (!isFolderOpen && event.getAction() == ACTION_HOVER_ENTER) { + // Do not reveal if any floating views such as folders or edu pop-ups are open. + revealHoverToolTip(); mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true); - return true; } - return false; - } - - private void startRevealHoverToolTip() { - mHoverToolTipHandler.post(mRevealHoverToolTipRunnable); + return true; } private void revealHoverToolTip() { if (mHoverView == null || mToolTipText == null) { return; } + // Do not show tooltip if taskbar icons are transitioning to hotseat. + if (mActivity.isIconAlignedWithHotseat()) { + return; + } if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) { return; } Rect iconViewBounds = Utilities.getViewBounds(mHoverView); mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(), - mTaskbarView.getTop(), /* shouldAutoClose= */ false); - } - - private void startHideHoverToolTip() { - int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis( - mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT); - mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout); - } - - private void hideHoverToolTip() { - mHoverToolTipView.close(/* animate = */ true); + mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index 2103ebbd10..685c109eec 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -56,7 +56,6 @@ import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Executors import java.io.PrintWriter import kotlin.jvm.optionals.getOrNull -import kotlin.math.max /** Handles the insets that Taskbar provides to underlying apps and the IME. */ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController { @@ -64,6 +63,10 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas companion object { private const val INDEX_LEFT = 0 private const val INDEX_RIGHT = 1 + + private fun Region.addBoundsToRegion(bounds: Rect?) { + bounds?.let { op(it, Region.Op.UNION) } + } } /** The bottom insets taskbar provides to the IME when IME is visible. */ @@ -102,7 +105,8 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas } fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() { - val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps + val taskbarStashController = controllers.taskbarStashController + val tappableHeight = taskbarStashController.tappableHeightToReportToApps // We only report tappableElement height for unstashed, persistent taskbar, // which is also when we draw the rounded corners above taskbar. val insetsRoundedCornerFlag = @@ -128,47 +132,31 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas } } - val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight + val bubbleControllers = controllers.bubbleControllers.getOrNull() + val taskbarTouchableHeight = taskbarStashController.touchableHeight val bubblesTouchableHeight = - if (controllers.bubbleControllers.isPresent) { - controllers.bubbleControllers.get().bubbleStashController.touchableHeight - } else { - 0 + bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0 + // reset touch bounds + defaultTouchableRegion.setEmpty() + if (bubbleControllers != null) { + val bubbleBarViewController = bubbleControllers.bubbleBarViewController + val isBubbleBarVisible = bubbleControllers.bubbleStashController.isBubbleBarVisible() + val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble + // if bubble bar is visible or animating new bubble, add bar bounds to the touch region + if (isBubbleBarVisible || isAnimatingNewBubble) { + defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds) } - val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight) - + } if ( - controllers.bubbleControllers.isPresent && - controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome + taskbarStashController.isInApp || + taskbarStashController.isInOverview || + DisplayController.showLockedTaskbarOnHome(context) ) { - val iconBounds = - controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds - defaultTouchableRegion.set( - iconBounds.left, - iconBounds.top, - iconBounds.right, - iconBounds.bottom - ) - } else { - defaultTouchableRegion.set( - 0, - windowLayoutParams.height - touchableHeight, - context.deviceProfile.widthPx, - windowLayoutParams.height - ) - - // if there's an animating bubble add it to the touch region so that it's clickable - val isAnimatingNewBubble = - controllers.bubbleControllers - .getOrNull() - ?.bubbleBarViewController - ?.isAnimatingNewBubble - ?: false - if (isAnimatingNewBubble) { - val iconBounds = - controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds - defaultTouchableRegion.op(iconBounds, Region.Op.UNION) - } + // only add the taskbar touch region if not on home + val bottom = windowLayoutParams.height + val top = bottom - taskbarTouchableHeight + val right = context.deviceProfile.widthPx + defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom)) } // Pre-calculate insets for different providers across different rotations for this gravity @@ -238,20 +226,20 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) } else if (provider.type == mandatorySystemGestures()) { if (context.isThreeButtonNav) { - provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, - endRotation) + provider.insetsSize = + getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) } else { val gestureHeight = - ResourceUtils.getNavbarSize( + ResourceUtils.getNavbarSize( ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, - context.resources) - val isPinnedTaskbar = context.deviceProfile.isTaskbarPresent - && !context.deviceProfile.isTransientTaskbar - val mandatoryGestureHeight = - if (isPinnedTaskbar) contentHeight - else gestureHeight - provider.insetsSize = getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, - endRotation) + context.resources + ) + val isPinnedTaskbar = + context.deviceProfile.isTaskbarPresent && + !context.deviceProfile.isTransientTaskbar + val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight + provider.insetsSize = + getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation) } } else if (provider.type == tappableElement()) { provider.insetsSize = getInsetsForGravity(tappableHeight, gravity) @@ -358,13 +346,6 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas */ fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) { insetsInfo.touchableRegion.setEmpty() - // Always have nav buttons be touchable - controllers.navbarButtonsViewController.addVisibleButtonsRegion( - context.dragLayer, - insetsInfo.touchableRegion - ) - debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds) - val bubbleBarVisible = controllers.bubbleControllers.isPresent && controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible() @@ -426,7 +407,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas // Include the bounds of the bubble bar in the touchable region if they exist. if (bubbleBarBounds != null) { - region.op(bubbleBarBounds, Region.Op.UNION) + region.addBoundsToRegion(bubbleBarBounds) } insetsInfo.touchableRegion.set(region) debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview" @@ -443,6 +424,12 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas debugTouchableRegion.lastSetTouchableReason = "Icons are not visible, but other components such as 3 buttons might be" } + // Always have nav buttons be touchable + controllers.navbarButtonsViewController.addVisibleButtonsRegion( + context.dragLayer, + insetsInfo.touchableRegion + ) + debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds) context.excludeFromMagnificationRegion(insetsIsTouchableRegion) } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index cb9f24ae80..876221b1d7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -16,13 +16,18 @@ package com.android.launcher3.taskbar; import static com.android.app.animation.Interpolators.EMPHASIZED; +import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT; +import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH; +import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE; +import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME; import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK; import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE; @@ -39,6 +44,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Hotseat.HotseatQsbAlphaId; import com.android.launcher3.LauncherState; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.Utilities; @@ -47,6 +53,7 @@ import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.RecentsAnimationController; @@ -143,9 +150,10 @@ public class TaskbarLauncherStateController { private AnimatedFloat mTaskbarBackgroundAlpha; private AnimatedFloat mTaskbarAlpha; private AnimatedFloat mTaskbarCornerRoundness; - private MultiProperty mIconAlphaForHome; + private MultiProperty mTaskbarAlphaForHome; private QuickstepLauncher mLauncher; + private boolean mIsDestroyed = false; private Integer mPrevState; private int mState; private LauncherState mLauncherState = LauncherState.NORMAL; @@ -172,11 +180,11 @@ public class TaskbarLauncherStateController { if (mIsQsbInline && !dp.isQsbInline) { // We only modify QSB alpha if isQsbInline = true. If we switch to a DP // where isQsbInline = false, then we need to reset the alpha. - mLauncher.getHotseat().setQsbAlpha(1f); + mLauncher.getHotseat().setQsbAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } mIsQsbInline = dp.isQsbInline; TaskbarLauncherStateController.this.updateIconAlphaForHome( - mIconAlphaForHome.getValue()); + mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } }; @@ -239,31 +247,34 @@ public class TaskbarLauncherStateController { .getTaskbarBackgroundAlpha(); mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha(); mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness(); - mIconAlphaForHome = mControllers.taskbarViewController + mTaskbarAlphaForHome = mControllers.taskbarViewController .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME); resetIconAlignment(); - mLauncher.getStateManager().addStateListener(mStateListener); + if (!mControllers.taskbarActivityContext.isPhoneMode()) { + mLauncher.getStateManager().addStateListener(mStateListener); + } mLauncherState = launcher.getStateManager().getState(); updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false); applyState(0); - mCanSyncViews = true; + mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode(); mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); updateOverviewDragState(mLauncherState); } public void onDestroy() { + mIsDestroyed = true; mCanSyncViews = false; mIconAlignment.finishAnimation(); - mLauncher.getHotseat().setIconsAlpha(1f); + mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); mLauncher.getStateManager().removeStateListener(mStateListener); - mCanSyncViews = true; + mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode(); mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); } @@ -289,6 +300,7 @@ public class TaskbarLauncherStateController { stashController.updateStateForFlag(FLAG_IN_APP, false); updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, true); + mLauncherState = toState; animatorSet.play(stashController.createApplyStateAnimator(duration)); animatorSet.play(applyState(duration, false)); @@ -349,8 +361,10 @@ public class TaskbarLauncherStateController { // 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. - boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING) + // hide/reveal animation timings. The Taskbar can show when dreaming if the glanceable hub + // is showing on top. + boolean isTaskbarHidden = (hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING) + && !hasAnyFlag(systemUiStateFlags, SYSUI_STATE_COMMUNAL_HUB_SHOWING)) || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE; updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden); @@ -367,7 +381,7 @@ public class TaskbarLauncherStateController { private void updateOverviewDragState(LauncherState launcherState) { boolean disallowLongClick = FeatureFlags.enableSplitContextually() - ? mLauncher.isSplitSelectionActive() + ? mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT; com.android.launcher3.taskbar.Utilities.setOverviewDragState( mControllers, launcherState.disallowTaskbarGlobalDrag(), @@ -407,7 +421,7 @@ public class TaskbarLauncherStateController { } public Animator applyState(long duration, boolean start) { - if (mControllers.taskbarActivityContext.isDestroyed()) { + if (mIsDestroyed || mControllers.taskbarActivityContext.isPhoneMode()) { return null; } Animator animator = null; @@ -444,14 +458,16 @@ public class TaskbarLauncherStateController { + ", toAlignment: " + toAlignment); } mControllers.bubbleControllers.ifPresent(controllers -> { - // Show the bubble bar when on launcher home or in overview. - boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL; + // Show the bubble bar when on launcher home (hotseat icons visible) or in overview boolean onOverview = mLauncherState == LauncherState.OVERVIEW; - controllers.bubbleStashController.setBubblesShowingOnHome(onHome); + boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible( + mLauncher, HOTSEAT_ICONS); + controllers.bubbleStashController.setBubblesShowingOnHome(hotseatIconsVisible); controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview); }); - mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_OVERVIEW, + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, mLauncherState == LauncherState.OVERVIEW); AnimatorSet animatorSet = new AnimatorSet(); @@ -470,7 +486,8 @@ public class TaskbarLauncherStateController { // We're changing state to home, should close open popups e.g. Taskbar AllApps handleOpenFloatingViews = true; } - if (mLauncherState == LauncherState.OVERVIEW) { + if (mLauncherState == LauncherState.OVERVIEW + && !mControllers.taskbarActivityContext.isPhoneMode()) { // Calling to update the insets in TaskbarInsetController#updateInsetsTouchability mControllers.taskbarActivityContext.notifyUpdateLayoutParams(); } @@ -482,8 +499,6 @@ public class TaskbarLauncherStateController { public void onAnimationStart(Animator animation) { mIsAnimatingToLauncher = isInLauncher; - TaskbarStashController stashController = - mControllers.taskbarStashController; if (DEBUG) { Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher); } @@ -499,6 +514,8 @@ public class TaskbarLauncherStateController { // Handle closing open popups when going home/overview handleOpenFloatingViews = true; + } else { + stashController.applyState(); } if (handleOpenFloatingViews && isInLauncher) { @@ -579,6 +596,13 @@ public class TaskbarLauncherStateController { float cornerRoundness = isInLauncher ? 0 : 1; + if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible() + && mControllers.getSharedState() != null) { + cornerRoundness = + mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness( + mControllers.getSharedState().showCornerRadiusInDesktopMode); + } + // Don't animate if corner roundness has reached desired value. if (mTaskbarCornerRoundness.isAnimating() || mTaskbarCornerRoundness.value != cornerRoundness) { @@ -640,6 +664,9 @@ public class TaskbarLauncherStateController { * This refers to the intended state - a transition to this state might be in progress. */ public boolean isTaskbarAlignedWithHotseat() { + if (DisplayController.showLockedTaskbarOnHome(mLauncher) && isInLauncher()) { + return false; + } return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); } @@ -651,8 +678,7 @@ public class TaskbarLauncherStateController { boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); boolean willStashVisually = isInStashedState && mControllers.taskbarStashController.supportsVisualStashing(); - boolean isTaskbarAlignedWithHotseat = - mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); + boolean isTaskbarAlignedWithHotseat = isTaskbarAlignedWithHotseat(); return isTaskbarAlignedWithHotseat && !willStashVisually; } else { return false; @@ -683,14 +709,17 @@ public class TaskbarLauncherStateController { public void onAnimationEnd(Animator animation) { if (isInStashedState && committed) { // Reset hotseat alpha to default - mLauncher.getHotseat().setIconsAlpha(1); + mLauncher.getHotseat().setIconsAlpha(1, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } } @Override public void onAnimationStart(Animator animation) { - if (mLauncher.getHotseat().getIconsAlpha() > 0) { - updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha()); + float hotseatIconsAlpha = mLauncher.getHotseat() + .getIconsAlpha(ALPHA_CHANNEL_TASKBAR_ALIGNMENT) + .getValue(); + if (hotseatIconsAlpha > 0) { + updateIconAlphaForHome(hotseatIconsAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } } }); @@ -719,6 +748,33 @@ public class TaskbarLauncherStateController { } } + protected void stashHotseat(boolean stash) { + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, stash); + Runnable swapHotseatWithTaskbar = new Runnable() { + @Override + public void run() { + updateIconAlphaForHome(stash ? 1 : 0, ALPHA_CHANNEL_TASKBAR_STASH); + } + }; + if (stash) { + stashController.applyState(); + // if we stashing the hotseat we need to immediately swap it with the animating taskbar + swapHotseatWithTaskbar.run(); + } else { + // if we revert stashing make swap after taskbar animation is complete + stashController.applyState(/* postApplyAction = */ swapHotseatWithTaskbar); + } + } + + protected void unStashHotseatInstantly() { + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, false); + stashController.applyState(/* duration = */ 0); + updateIconAlphaForHome(/* taskbarAlpha = */ 0, + ALPHA_CHANNEL_TASKBAR_STASH, /* updateTaskbarAlpha = */ false); + } + /** * Resets and updates the icon alignment. */ @@ -728,7 +784,7 @@ public class TaskbarLauncherStateController { } private void onIconAlignmentRatioChanged() { - float currentValue = mIconAlphaForHome.getValue(); + float currentValue = mTaskbarAlphaForHome.getValue(); boolean taskbarWillBeVisible = mIconAlignment.value < 1; boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0) || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0); @@ -736,24 +792,33 @@ public class TaskbarLauncherStateController { mControllers.taskbarViewController.setLauncherIconAlignment( mIconAlignment.value, mLauncher.getDeviceProfile()); mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value); - // Switch taskbar and hotseat in last frame - updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0); + // Switch taskbar and hotseat in last frame and if taskbar is not hidden for bubbles + boolean isHiddenForBubbles = mControllers.taskbarStashController.isHiddenForBubbles(); + updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0, ALPHA_CHANNEL_TASKBAR_ALIGNMENT, + /* updateTaskbarAlpha = */ !isHiddenForBubbles); // Sync the first frame where we swap taskbar and hotseat. if (firstFrameVisChanged && mCanSyncViews && !Utilities.isRunningInTestHarness()) { ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(), mControllers.taskbarActivityContext.getDragLayer(), - () -> { - }); + () -> {}); } } - private void updateIconAlphaForHome(float alpha) { - if (mControllers.taskbarActivityContext.isDestroyed()) { + private void updateIconAlphaForHome(float taskbarAlpha, @HotseatQsbAlphaId int alphaChannel) { + updateIconAlphaForHome(taskbarAlpha, alphaChannel, /* updateTaskbarAlpha = */ true); + } + + private void updateIconAlphaForHome(float taskbarAlpha, + @HotseatQsbAlphaId int alphaChannel, + boolean updateTaskbarAlpha) { + if (mIsDestroyed) { return; } - mIconAlphaForHome.setValue(alpha); - boolean hotseatVisible = alpha == 0 + if (updateTaskbarAlpha) { + mTaskbarAlphaForHome.setValue(taskbarAlpha); + } + boolean hotseatVisible = taskbarAlpha == 0 || mControllers.taskbarActivityContext.isPhoneMode() || (!mControllers.uiController.isHotseatIconOnTopWhenAligned() && mIconAlignment.value > 0); @@ -761,9 +826,10 @@ public class TaskbarLauncherStateController { * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets * should not be visible at the same time. */ - mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0); + float targetAlpha = hotseatVisible ? 1 : 0; + mLauncher.getHotseat().setIconsAlpha(targetAlpha, alphaChannel); if (mIsQsbInline) { - mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0); + mLauncher.getHotseat().setQsbAlpha(targetAlpha, alphaChannel); } } @@ -851,8 +917,9 @@ public class TaskbarLauncherStateController { pw.println(String.format( "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value)); pw.println(String.format( - "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue())); - pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState))); + "%s\tmTaskbarAlphaForHome=%.2f", prefix, mTaskbarAlphaForHome.getValue())); + pw.println(String.format("%s\tmPrevState=%s", prefix, + mPrevState == null ? null : getStateString(mPrevState))); pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState))); pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState)); pw.println(String.format( diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 2a58db25df..78e7b470ff 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -39,7 +39,6 @@ import android.app.PendingIntent; import android.content.ComponentCallbacks; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.hardware.display.DisplayManager; @@ -61,6 +60,8 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider; @@ -72,6 +73,7 @@ import com.android.quickstep.AllAppsActionManager; import com.android.quickstep.RecentsActivity; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AssistUtils; +import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; @@ -114,11 +116,12 @@ public class TaskbarManager { private WindowManager mWindowManager; private FrameLayout mTaskbarRootLayout; private boolean mAddedWindow; + private boolean mIsSuspended; private final TaskbarNavButtonController mNavButtonController; private final ComponentCallbacks mComponentCallbacks; private final SimpleBroadcastReceiver mShutdownReceiver = - new SimpleBroadcastReceiver(i -> destroyExistingTaskbar()); + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyExistingTaskbar()); // The source for this provider is set when Launcher is available // We use 'non-destroyable' version here so the original provider won't be destroyed @@ -155,7 +158,7 @@ public class TaskbarManager { private boolean mUserUnlocked = false; private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver = - new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast); + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast); private final AllAppsActionManager mAllAppsActionManager; @@ -208,12 +211,14 @@ public class TaskbarManager { } }; + @NonNull private final DesktopVisibilityController mDesktopVisibilityController; + @SuppressLint("WrongConstant") public TaskbarManager( Context context, AllAppsActionManager allAppsActionManager, - TaskbarNavButtonCallbacks navCallbacks) { - + TaskbarNavButtonCallbacks navCallbacks, + @NonNull DesktopVisibilityController desktopVisibilityController) { Display display = context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); mContext = context.createWindowContext(display, @@ -223,6 +228,7 @@ public class TaskbarManager { mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION ? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null) : null; + mDesktopVisibilityController = desktopVisibilityController; if (enableTaskbarNoRecreate()) { mWindowManager = mContext.getSystemService(WindowManager.class); mTaskbarRootLayout = new FrameLayout(mContext) { @@ -242,6 +248,7 @@ public class TaskbarManager { context, navCallbacks, SystemUiProxy.INSTANCE.get(mContext), + ContextualEduStatsManager.INSTANCE.get(mContext), new Handler(), AssistUtils.newInstance(mContext)); mComponentCallbacks = new ComponentCallbacks() { @@ -311,10 +318,8 @@ public class TaskbarManager { SYSTEM_ACTION_ID_TASKBAR, new Intent(ACTION_SHOW_TASKBAR).setPackage(mContext.getPackageName()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - mContext.registerReceiver( - mTaskbarBroadcastReceiver, - new IntentFilter(ACTION_SHOW_TASKBAR), - RECEIVER_NOT_EXPORTED); + mTaskbarBroadcastReceiver.register( + mContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR); }); debugWhyTaskbarNotDestroyed("TaskbarManager created"); @@ -442,6 +447,8 @@ public class TaskbarManager { */ @VisibleForTesting public synchronized void recreateTaskbar() { + if (mIsSuspended) return; + Trace.beginSection("recreateTaskbar"); try { DeviceProfile dp = mUserUnlocked ? @@ -458,16 +465,18 @@ public class TaskbarManager { + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null) + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent)); - if (!isTaskbarEnabled) { + if (!isTaskbarEnabled || !isLargeScreenTaskbar) { SystemUiProxy.INSTANCE.get(mContext) .notifyTaskbarStatus(/* visible */ false, /* stashed */ false); - return; + if (!isTaskbarEnabled) { + return; + } } if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) { mTaskbarActivityContext = new TaskbarActivityContext(mContext, mNavigationBarPanelContext, dp, mNavButtonController, - mUnfoldProgressProvider); + mUnfoldProgressProvider, mDesktopVisibilityController); } else { mTaskbarActivityContext.updateDeviceProfile(dp); } @@ -519,6 +528,44 @@ public class TaskbarManager { } } + public void setWallpaperVisible(boolean isVisible) { + mSharedState.wallpaperVisible = isVisible; + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.setWallpaperVisible(isVisible); + } + } + + public void checkNavBarModes() { + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.checkNavBarModes(); + } + } + + public void finishBarAnimations() { + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.finishBarAnimations(); + } + } + + public void touchAutoDim(boolean reset) { + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.touchAutoDim(reset); + } + } + + public void transitionTo(@BarTransitions.TransitionMode int barMode, + boolean animate) { + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.transitionTo(barMode, animate); + } + } + + public void appTransitionPending(boolean pending) { + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.appTransitionPending(pending); + } + } + private boolean isTaskbarEnabled(DeviceProfile deviceProfile) { return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent; } @@ -546,6 +593,13 @@ public class TaskbarManager { } } + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + mSharedState.barMode = barMode; + if (mTaskbarActivityContext != null) { + mTaskbarActivityContext.onTransitionModeUpdated(barMode, checkBarModes); + } + } + public void onNavButtonsDarkIntensityChanged(float darkIntensity) { mSharedState.navButtonsDarkIntensity = darkIntensity; if (mTaskbarActivityContext != null) { @@ -582,8 +636,7 @@ public class TaskbarManager { public void destroy() { debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()"); removeActivityCallbacksAndListeners(); - UI_HELPER_EXECUTOR.execute( - () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext)); + mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext); destroyExistingTaskbar(); removeTaskbarRootViewFromWindow(); if (mUserUnlocked) { @@ -595,7 +648,7 @@ public class TaskbarManager { .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener); Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy()."); mContext.unregisterComponentCallbacks(mComponentCallbacks); - mContext.unregisterReceiver(mShutdownReceiver); + mShutdownReceiver.unregisterReceiverSafely(mContext); } public @Nullable TaskbarActivityContext getCurrentActivityContext() { @@ -611,8 +664,22 @@ public class TaskbarManager { } } + /** + * Removes Taskbar from the window manager and prevents recreation if {@code true}. + *

+ * Suspending is for testing purposes only; avoid calling this method in production. + */ @VisibleForTesting - void addTaskbarRootViewToWindow() { + public void setSuspended(boolean isSuspended) { + mIsSuspended = isSuspended; + if (mIsSuspended) { + removeTaskbarRootViewFromWindow(); + } else { + addTaskbarRootViewToWindow(); + } + } + + private void addTaskbarRootViewToWindow() { if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) { mWindowManager.addView(mTaskbarRootLayout, mTaskbarActivityContext.getWindowLayoutParams()); @@ -620,8 +687,7 @@ public class TaskbarManager { } } - @VisibleForTesting - void removeTaskbarRootViewFromWindow() { + private void removeTaskbarRootViewFromWindow() { if (enableTaskbarNoRecreate() && mAddedWindow) { mWindowManager.removeViewImmediate(mTaskbarRootLayout); mAddedWindow = false; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 2b0e1699cc..bdefea6b22 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -15,9 +15,6 @@ */ package com.android.launcher3.taskbar; -import static com.android.window.flags.Flags.enableDesktopWindowingMode; -import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; - import android.util.SparseArray; import android.view.View; @@ -29,7 +26,6 @@ import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; @@ -37,8 +33,7 @@ import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; -import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.GroupTask; import java.io.PrintWriter; import java.util.ArrayList; @@ -54,7 +49,7 @@ import java.util.function.Predicate; * Launcher model Callbacks for rendering taskbar. */ public class TaskbarModelCallbacks implements - BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener { + BgDataModel.Callbacks, LauncherBindableItemsContainer { private final SparseArray mHotseatItems = new SparseArray<>(); private List mPredictedItems = Collections.emptyList(); @@ -68,8 +63,6 @@ public class TaskbarModelCallbacks implements // Used to defer any UI updates during the SUW unstash animation. private boolean mDeferUpdatesForSUW; private Runnable mDeferredUpdates; - private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener = - visible -> updateRunningApps(); public TaskbarModelCallbacks( TaskbarActivityContext context, TaskbarView container) { @@ -79,39 +72,6 @@ public class TaskbarModelCallbacks implements public void init(TaskbarControllers controllers) { mControllers = controllers; - if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) { - RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.registerDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - } - - /** - * Unregisters listeners in this class. - */ - public void unregisterListeners() { - RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener(); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.unregisterDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - - private boolean shouldShowRunningAppsInDesktopMode() { - // TODO(b/335401172): unify DesktopMode checks in Launcher - return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps(); } @Override @@ -171,7 +131,7 @@ public class TaskbarModelCallbacks implements final int itemCount = mContainer.getChildCount(); for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { View item = mContainer.getChildAt(itemIdx); - if (op.evaluate((ItemInfo) item.getTag(), item)) { + if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) { return; } } @@ -232,26 +192,30 @@ public class TaskbarModelCallbacks implements predictionNextIndex++; } } - hotseatItemInfos = mControllers.taskbarRecentAppsController - .updateHotseatItemInfos(hotseatItemInfos); - Set runningPackages = mControllers.taskbarRecentAppsController.getRunningApps(); - Set minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps(); + + final TaskbarRecentAppsController recentAppsController = + mControllers.taskbarRecentAppsController; + hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos); + Set runningTaskIds = recentAppsController.getRunningTaskIds(); + Set minimizedTaskIds = recentAppsController.getMinimizedTaskIds(); if (mDeferUpdatesForSUW) { ItemInfo[] finalHotseatItemInfos = hotseatItemInfos; mDeferredUpdates = () -> - commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages, - minimizedPackages); + commitHotseatItemUpdates(finalHotseatItemInfos, + recentAppsController.getShownTasks(), runningTaskIds, + minimizedTaskIds); } else { - commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages); + commitHotseatItemUpdates(hotseatItemInfos, + recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds); } } - private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set runningPackages, - Set minimizedPackages) { - mContainer.updateHotseatItems(hotseatItemInfos); - mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages, - minimizedPackages); + private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List recentTasks, + Set runningTaskIds, Set minimizedTaskIds) { + mContainer.updateHotseatItems(hotseatItemInfos, recentTasks); + mControllers.taskbarViewController.updateIconViewsRunningStates( + runningTaskIds, minimizedTaskIds); } /** @@ -270,21 +234,11 @@ public class TaskbarModelCallbacks implements } } - @Override - public void onRunningTasksChanged() { - updateRunningApps(); - } - /** Called when there's a change in running apps to update the UI. */ public void commitRunningAppsToUI() { commitItemsToUI(); } - /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */ - public void updateRunningApps() { - mControllers.taskbarRecentAppsController.updateRunningApps(); - } - @Override public void bindDeepShortcutMap(HashMap deepShortcutMapCopy) { mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy); @@ -296,7 +250,7 @@ public class TaskbarModelCallbacks implements Map packageUserKeytoUidMap) { Preconditions.assertUIThread(); mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap); - mControllers.taskbarRecentAppsController.setApps(apps); + mControllers.taskbarPopupController.setApps(apps); } protected void dumpLogs(String prefix, PrintWriter pw) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index d26a36d175..15c35b644e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -24,6 +24,7 @@ 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; @@ -37,18 +38,21 @@ import android.os.Handler; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.View; +import android.view.inputmethod.Flags; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.android.launcher3.R; +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.util.AssistUtils; +import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import java.io.PrintWriter; @@ -107,6 +111,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private final Context mContext; private final TaskbarNavButtonCallbacks mCallbacks; private final SystemUiProxy mSystemUiProxy; + private final ContextualEduStatsManager mContextualEduStatsManager; private final Handler mHandler; private final AssistUtils mAssistUtils; @Nullable private StatsLogManager mStatsLogManager; @@ -117,11 +122,13 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa Context context, TaskbarNavButtonCallbacks callbacks, SystemUiProxy systemUiProxy, + ContextualEduStatsManager contextualEduStatsManager, Handler handler, AssistUtils assistUtils) { mContext = context; mCallbacks = callbacks; mSystemUiProxy = systemUiProxy; + mContextualEduStatsManager = contextualEduStatsManager; mHandler = handler; mAssistUtils = assistUtils; } @@ -135,19 +142,25 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa switch (buttonType) { case BUTTON_BACK: logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); + mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false, + GestureType.BACK); executeBack(); break; case BUTTON_HOME: logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); + mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false, + GestureType.HOME); navigateHome(); break; case BUTTON_RECENTS: logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP); + mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false, + GestureType.OVERVIEW); navigateToOverview(); break; case BUTTON_IME_SWITCH: logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); - showIMESwitcher(); + onImeSwitcherPress(); break; case BUTTON_A11Y: logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP); @@ -166,8 +179,12 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa if (buttonType == BUTTON_SPACE) { return false; } - // Provide the same haptic feedback that the system offers for virtual keys. - view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + + // Provide the same haptic feedback that the system offers for long press. + // The haptic feedback from long pressing on the home button is handled by circle to search. + if (buttonType != BUTTON_HOME) { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } switch (buttonType) { case BUTTON_HOME: logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); @@ -179,11 +196,19 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa return true; case BUTTON_BACK: logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); - return backRecentsLongpress(buttonType); + backRecentsLongpress(buttonType); + return true; case BUTTON_RECENTS: logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS); - return backRecentsLongpress(buttonType); + backRecentsLongpress(buttonType); + return true; case BUTTON_IME_SWITCH: + if (Flags.imeSwitcherRevamp()) { + logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + onImeSwitcherLongPress(); + return true; + } + return false; default: return false; } @@ -299,10 +324,14 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa mSystemUiProxy.onBackPressed(); } - private void showIMESwitcher() { + private void onImeSwitcherPress() { mSystemUiProxy.onImeSwitcherPressed(); } + private void onImeSwitcherLongPress() { + mSystemUiProxy.onImeSwitcherLongPress(); + } + private void notifyA11yClick(boolean longClick) { if (longClick) { mSystemUiProxy.notifyAccessibilityButtonLongClicked(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt index 6c9cc642be..1867cd08c0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt @@ -32,10 +32,8 @@ import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAnd import java.io.PrintWriter /** Controls taskbar pinning through a popup view. */ -class TaskbarPinningController( - private val context: TaskbarActivityContext, - private val isInDesktopModeProvider: () -> Boolean, -) : TaskbarControllers.LoggableTaskbarController { +class TaskbarPinningController(private val context: TaskbarActivityContext) : + TaskbarControllers.LoggableTaskbarController { private lateinit var controllers: TaskbarControllers private lateinit var taskbarSharedState: TaskbarSharedState @@ -58,7 +56,7 @@ class TaskbarPinningController( return } val shouldPinTaskbar = - if (isInDesktopModeProvider()) { + if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) { !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE) } else { !launcherPrefs.get(TASKBAR_PINNING) @@ -119,7 +117,7 @@ class TaskbarPinningController( dragLayerController.taskbarBackgroundProgress.animateToValue(animateToValue), taskbarViewController.taskbarIconTranslationYForPinning.animateToValue(animateToValue), taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue), - taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue) + taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue), ) animatorSet.interpolator = Interpolators.EMPHASIZED @@ -134,10 +132,10 @@ class TaskbarPinningController( @VisibleForTesting fun recreateTaskbarAndUpdatePinningValue() { updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false) - if (isInDesktopModeProvider()) { + if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) { launcherPrefs.put( TASKBAR_PINNING_IN_DESKTOP_MODE, - !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE) + !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE), ) } else { launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING)) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java index 2730be1b57..70d4bb10f2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; import android.content.Intent; @@ -29,11 +30,13 @@ import androidx.annotation.NonNull; import com.android.internal.logging.InstanceId; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.dot.FolderDotInfo; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -51,8 +54,11 @@ import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.views.ActivityContext; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.LogUtils; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -69,12 +75,16 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba private static final SystemShortcut.Factory APP_INFO = SystemShortcut.AppInfo::new; + private static final SystemShortcut.Factory + BUBBLE = SystemShortcut.BubbleShortcut::new; + private final TaskbarActivityContext mContext; private final PopupDataProvider mPopupDataProvider; // Initialized in init. private TaskbarControllers mControllers; private boolean mAllowInitialSplitSelection; + private AppInfo[] mAppInfosList; public TaskbarPopupController(TaskbarActivityContext context) { mContext = context; @@ -148,8 +158,8 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba icon.clearFocus(); return null; } - ItemInfo item = (ItemInfo) icon.getTag(); - if (!ShortcutUtil.supportsShortcuts(item)) { + // TODO(b/344657629) support GroupTask as well, for Taskbar Recent apps + if (!(icon.getTag() instanceof ItemInfo item) || !ShortcutUtil.supportsShortcuts(item)) { return null; } @@ -181,11 +191,21 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba // Create a Stream of all applicable system shortcuts private Stream getSystemShortcuts() { - // append split options to APP_INFO shortcut, the order here will reflect in the popup - return Stream.concat( - Stream.of(APP_INFO), - mControllers.uiController.getSplitMenuOptions() - ); + // append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order + // here will reflect in the popup + ArrayList shortcuts = new ArrayList<>(); + shortcuts.add(APP_INFO); + if (!mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) { + shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList()); + } + if (com.android.wm.shell.Flags.enableBubbleAnything()) { + shortcuts.add(BUBBLE); + } + if (Flags.enableMultiInstanceMenuTaskbar() + && DesktopModeStatus.canEnterDesktopMode(mContext)) { + shortcuts.addAll(getMultiInstanceMenuOptions().toList()); + } + return shortcuts.stream(); } @Override @@ -248,7 +268,55 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba originalView, position, mAllowInitialSplitSelection); } - /** + /** + * Set the list of AppInfos to be able to pull from later + */ + public void setApps(AppInfo[] apps) { + mAppInfosList = apps; + } + + /** + * Finds and returns an AppInfo object from a list, using its ComponentKey for identification. + * Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)} + * since we cannot access AllAppsStore from here. + */ + public AppInfo getApp(ComponentKey key) { + if (key == null) { + return null; + } + AppInfo tempInfo = new AppInfo(); + tempInfo.componentName = key.componentName; + tempInfo.user = key.user; + int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR); + return index < 0 ? null : mAppInfosList[index]; + } + + /** + * Returns a stream of Multi Instance menu options if an app supports it. + */ + Stream> getMultiInstanceMenuOptions() { + SystemShortcut.Factory factory = createNewWindowShortcutFactory(); + return factory != null ? Stream.of(factory) : Stream.empty(); + + } + + /** + * Creates a factory function representing a "New Window" menu item only if the calling app + * supports multi-instance. + * @return A factory function to be used in populating the long-press menu. + */ + SystemShortcut.Factory createNewWindowShortcutFactory() { + return (context, itemInfo, originalView) -> { + ComponentKey key = itemInfo.getComponentKey(); + AppInfo app = getApp(key); + if (app != null && app.supportsMultiInstance()) { + return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView); + } + return null; + }; + } + + /** * A single menu item ("Split left," "Split right," or "Split top") that executes a split * from the taskbar, as if the user performed a drag and drop split. * Includes an onClick method that initiates the actual split. diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index b1fc9ccb02..57d4dbb80a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -15,18 +15,19 @@ */ package com.android.launcher3.taskbar -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration +import android.content.Context +import android.window.flags.DesktopModeFlags import androidx.annotation.VisibleForTesting import com.android.launcher3.Flags.enableRecentsInTaskbar -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.statehandlers.DesktopVisibilityController import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.util.CancellableTask import com.android.quickstep.RecentsModel -import com.android.window.flags.Flags.enableDesktopWindowingMode -import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import java.io.PrintWriter /** @@ -34,153 +35,292 @@ import java.io.PrintWriter * - When in Fullscreen mode: show the N most recent Tasks * - When in Desktop Mode: show the currently running (open) Tasks */ -class TaskbarRecentAppsController( - private val recentsModel: RecentsModel, - // Pass a provider here instead of the actual DesktopVisibilityController instance since that - // instance might not be available when this constructor is called. - private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?, -) : LoggableTaskbarController { +class TaskbarRecentAppsController(context: Context, private val recentsModel: RecentsModel) : + LoggableTaskbarController { - // TODO(b/335401172): unify DesktopMode checks in Launcher. - val canShowRunningApps = - enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps() + var canShowRunningApps = + DesktopModeStatus.canEnterDesktopMode(context) && + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue + @VisibleForTesting + set(isEnabledFromTest) { + field = isEnabledFromTest + if (!field && !canShowRecentApps) { + recentsModel.unregisterRecentTasksChangedListener() + } + } // TODO(b/343532825): Add a setting to disable Recents even when the flag is on. - var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps + var canShowRecentApps = enableRecentsInTaskbar() @VisibleForTesting - set(isEnabledFromTest){ + set(isEnabledFromTest) { field = isEnabledFromTest + if (!field && !canShowRunningApps) { + recentsModel.unregisterRecentTasksChangedListener() + } } // Initialized in init. private lateinit var controllers: TaskbarControllers - private var apps: Array? = null - private var allRunningDesktopAppInfos: List? = null - private var allMinimizedDesktopAppInfos: List? = null + var shownHotseatItems: List = emptyList() + private set - private val desktopVisibilityController: DesktopVisibilityController? - get() = desktopVisibilityControllerProvider() + private var allRecentTasks: List = emptyList() + private var desktopTask: DesktopTask? = null + // Keeps track of the order in which running tasks appear. + private var orderedRunningTaskIds = emptyList() + var shownTasks: List = emptyList() + private set - private val isInDesktopMode: Boolean - get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false - - val runningApps: Set + val runningTaskIds: Set + /** + * Returns the task IDs of apps that should be indicated as "running" to the user. + * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet(). + */ get() { - if (!isEnabled || !isInDesktopMode) { + if ( + !canShowRunningApps || + !controllers.taskbarDesktopModeController.areDesktopTasksVisible + ) { return emptySet() } - return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet() + val tasks = desktopTask?.tasks ?: return emptySet() + return tasks.map { task -> task.key.id }.toSet() } - val minimizedApps: Set + val minimizedTaskIds: Set + /** + * Returns the task IDs for the tasks that should be indicated as "minimized" to the user. + */ get() { - if (!isInDesktopMode) { + if ( + !canShowRunningApps || + !controllers.taskbarDesktopModeController.areDesktopTasksVisible + ) { return emptySet() } - return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() - ?: emptySet() + val desktopTasks = desktopTask?.tasks ?: return emptySet() + return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet() } + private val recentTasksChangedListener = + RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() } + + private val iconLoadRequests: MutableSet> = HashSet() + + // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?) + + // Used to keep track of the last requested task list ID, so that we do not request to load the + // tasks again if we have already requested it and the task list has not changed + private var taskListChangeId = -1 + fun init(taskbarControllers: TaskbarControllers) { controllers = taskbarControllers + if (canShowRunningApps || canShowRecentApps) { + recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener) + controllers.runAfterInit { reloadRecentTasksIfNeeded() } + } } fun onDestroy() { - apps = null - } - - /** Stores the current [AppInfo] instances, no-op except in desktop environment. */ - fun setApps(apps: Array?) { - this.apps = apps + recentsModel.unregisterRecentTasksChangedListener() + iconLoadRequests.forEach { it.cancel() } + iconLoadRequests.clear() } /** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */ - // TODO(next CL): add new section of Tasks instead of changing Hotseat items fun updateHotseatItemInfos(hotseatItems: Array): Array { - if (!isEnabled || !isInDesktopMode) { + // Ignore predicted apps - we show running or recent apps instead. + val areDesktopTasksVisible = controllers.taskbarDesktopModeController.areDesktopTasksVisible + val removePredictions = + (areDesktopTasksVisible && canShowRunningApps) || + (!areDesktopTasksVisible && canShowRecentApps) + if (!removePredictions) { + shownHotseatItems = hotseatItems.filterNotNull() + onRecentsOrHotseatChanged() return hotseatItems } - val newHotseatItemInfos = + shownHotseatItems = hotseatItems .filterNotNull() - // Ignore predicted apps - we show running apps instead .filter { itemInfo -> !itemInfo.isPredictedItem } .toMutableList() - val runningDesktopAppInfos = - allRunningDesktopAppInfos?.let { - getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList()) + + if (areDesktopTasksVisible && canShowRunningApps) { + shownHotseatItems = + updateHotseatItemsFromRunningTasks( + getOrderedAndWrappedDesktopTasks(), + shownHotseatItems, + ) + } + + onRecentsOrHotseatChanged() + + return shownHotseatItems.toTypedArray() + } + + private fun getOrderedAndWrappedDesktopTasks(): List { + val tasks = desktopTask?.tasks ?: emptyList() + // Kind of hacky, we wrap each single task in the Desktop as a GroupTask. + val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index } + val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] }) + return sortedTasks.map { GroupTask(it) } + } + + private fun reloadRecentTasksIfNeeded() { + if (!recentsModel.isTaskListValid(taskListChangeId)) { + taskListChangeId = + recentsModel.getTasks { tasks -> + allRecentTasks = tasks + val oldRunningTaskdIds = runningTaskIds + val oldMinimizedTaskIds = minimizedTaskIds + desktopTask = allRecentTasks.filterIsInstance().firstOrNull() + val runningTasksChanged = oldRunningTaskdIds != runningTaskIds + val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds + if ( + onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged + ) { + controllers.taskbarViewController.commitRunningAppsToUI() + } + } + } + } + + /** + * Updates [shownTasks] when Recents or Hotseat changes. + * + * @return Whether [shownTasks] changed. + */ + private fun onRecentsOrHotseatChanged(): Boolean { + val oldShownTasks = shownTasks + orderedRunningTaskIds = updateOrderedRunningTaskIds() + shownTasks = + if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) { + computeShownRunningTasks() + } else { + computeShownRecentTasks() } - if (runningDesktopAppInfos != null) { - newHotseatItemInfos.addAll(runningDesktopAppInfos) - } - return newHotseatItemInfos.toTypedArray() - } - - private fun getRunningDesktopAppInfosExceptHotseatApps( - allRunningDesktopAppInfos: List, - hotseatItems: List - ): List { - val hotseatPackages = hotseatItems.map { it.targetPackage } - return allRunningDesktopAppInfos - .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) } - .map { WorkspaceItemInfo(it) } - } - - private fun getDesktopRunningTasks(): List = - recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo -> - taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM + val shownTasksChanged = oldShownTasks != shownTasks + if (!shownTasksChanged) { + return shownTasksChanged } - // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos - private fun getAppInfosFromRunningTasks(tasks: List): List { - // Early return if apps is empty, since we then have no AppInfo to compare to - if (apps == null) { + for (groupTask in shownTasks) { + for (task in groupTask.tasks) { + val cancellableTask = + recentsModel.iconCache.getIconInBackground(task) { + icon, + contentDescription, + title -> + task.icon = icon + task.titleDescription = contentDescription + task.title = title + controllers.taskbarViewController.onTaskUpdated(task) + } + if (cancellableTask != null) { + iconLoadRequests.add(cancellableTask) + } + } + } + return shownTasksChanged + } + + private fun updateOrderedRunningTaskIds(): MutableList { + val desktopTaskAsList = getOrderedAndWrappedDesktopTasks() + val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id } + var newOrder = + orderedRunningTaskIds + .filter { it in desktopTaskIds } // Only keep the tasks that are still running + .toMutableList() + // Add new tasks not already listed + newOrder.addAll(desktopTaskIds.filter { it !in newOrder }) + return newOrder + } + + private fun computeShownRunningTasks(): List { + if (!canShowRunningApps) { return emptyList() } - val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull() - return packageNames - .map { packageName -> apps?.find { app -> packageName == app.targetPackage } } - .filterNotNull() + val desktopTaskAsList = getOrderedAndWrappedDesktopTasks() + val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id } + val shownTaskIds = shownTasks.map { it.task1.key.id } + // TODO(b/315344726 Multi-instance support): only show one icon per package once we support + // taskbar multi-instance menus + val shownHotseatItemTaskIds = + shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId } + // Remove any newly-missing Tasks, and actual group-tasks + val newShownTasks = + shownTasks + .filter { !it.hasMultipleTasks() } + .filter { it.task1.key.id in desktopTaskIds } + .toMutableList() + // Add any new Tasks, maintaining the order from previous shownTasks. + newShownTasks.addAll(desktopTaskAsList.filter { it.task1.key.id !in shownTaskIds }) + // Remove any tasks already covered by Hotseat icons + return newShownTasks.filter { it.task1.key.id !in shownHotseatItemTaskIds } } - /** Called to update the list of currently running apps, no-op except in desktop environment. */ - fun updateRunningApps() { - if (!isEnabled || !isInDesktopMode) { - return controllers.taskbarViewController.commitRunningAppsToUI() + private fun computeShownRecentTasks(): List { + if (!canShowRecentApps || allRecentTasks.isEmpty()) { + return emptyList() } - val runningTasks = getDesktopRunningTasks() - val runningAppInfo = getAppInfosFromRunningTasks(runningTasks) - allRunningDesktopAppInfos = runningAppInfo - updateMinimizedApps(runningTasks, runningAppInfo) - controllers.taskbarViewController.commitRunningAppsToUI() + // Remove the current task. + val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1) + // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too + var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems) + if (shownTasks.size > MAX_RECENT_TASKS) { + // Remove any tasks older than MAX_RECENT_TASKS. + shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size) + } + return shownTasks } - private fun updateMinimizedApps( - runningTasks: List, - runningAppInfo: List, - ) { - val allRunningAppTasks = - runningAppInfo - .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } } - .associate { (appInfo, targetPackage) -> - appInfo to - runningTasks - .filter { it.realActivity?.packageName == targetPackage } - .map { it.taskId } - } - val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible } - allMinimizedDesktopAppInfos = - allRunningAppTasks - .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } } - .keys - .toList() + private fun dedupeHotseatTasks( + groupTasks: List, + shownHotseatItems: List, + ): List { + val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage } + return groupTasks.filter { groupTask -> + groupTask.hasMultipleTasks() || + !hotseatPackages.contains(groupTask.task1.key.packageName) + } } + /** + * Returns the hotseat items updated so that any item that points to a package with a running + * task also references that task. + */ + private fun updateHotseatItemsFromRunningTasks( + groupTasks: List, + shownHotseatItems: List, + ): List = + shownHotseatItems.map { itemInfo -> + if (itemInfo is TaskItemInfo) { + itemInfo + } else { + val foundTask = + groupTasks.find { task -> task.task1.key.packageName == itemInfo.targetPackage } + ?: return@map itemInfo + TaskItemInfo(foundTask.task1.key.id, itemInfo as WorkspaceItemInfo) + } + } + override fun dumpLogs(prefix: String, pw: PrintWriter) { pw.println("$prefix TaskbarRecentAppsController:") - pw.println("$prefix\tisEnabled=$isEnabled") pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps") - // TODO(next CL): add more logs + pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps") + pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}") + pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}") + pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}") + pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}") + pw.println("$prefix\trunningTaskIds=$runningTaskIds") + pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds") + } + + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } + + private companion object { + const val MAX_RECENT_TASKS = 2 } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java index 48d2bc2ff7..751a42ad31 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java @@ -20,14 +20,15 @@ import static android.view.View.VISIBLE; import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED; -import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; +import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import android.animation.ObjectAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.launcher3.util.DisplayController; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -65,6 +66,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa */ public void init(TaskbarControllers controllers) { mControllers = controllers; + onTaskbarVisibilityChanged(mControllers.taskbarViewController.getTaskbarVisibility()); } /** @@ -85,6 +87,10 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa * Updates the scrim state based on the flags. */ public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) { + if (mActivity.isPhoneMode()) { + // There is no scrim for the bar in the phone mode. + return; + } if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) { // These scrims aren't used if bubble bar & transient taskbar are active. return; @@ -96,10 +102,21 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa private boolean shouldShowScrim() { final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0; boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0; + BubbleControllers bubbleControllers = mActivity.getBubbleControllers(); + boolean isBubbleControllersPresented = bubbleControllers != null; + // when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand, + // which makes the taskbar invisible, so need to check if the bubble bar is not on home + // to show the scrim view + boolean showScrimForBubbles = bubblesExpanded + && !mTaskbarVisible + && isBubbleControllersPresented + && !DisplayController.isTransientTaskbar(mActivity) + && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome(); return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible() && !isShadeVisible && !mControllers.taskbarStashController.isStashed() - && mTaskbarVisible; + && (mTaskbarVisible || showScrimForBubbles) + && !mControllers.taskbarStashController.isHiddenForBubbles(); } private float getScrimAlpha() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java index edaeb63381..729cbe951f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java @@ -56,12 +56,17 @@ public class TaskbarSharedState { // TaskbarManager#onNavButtonsDarkIntensityChanged() public float navButtonsDarkIntensity; + // TaskbarManager#onTransitionModeUpdated() + public int barMode; + // TaskbarManager#onNavigationBarLumaSamplingEnabled() public int mLumaSamplingDisplayId = DEFAULT_DISPLAY; public boolean mIsLumaSamplingEnabled = true; public boolean setupUIVisible = false; + public boolean wallpaperVisible = false; + public boolean allAppsVisible = false; // LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp @@ -97,5 +102,8 @@ public class TaskbarSharedState { // To track if taskbar was stashed / unstashed between configuration changes (which recreates // the task bar). - public Boolean taskbarWasStashedAuto = true; + public boolean taskbarWasStashedAuto = true; + + // should show corner radius on persistent taskbar when in desktop mode. + public boolean showCornerRadiusInDesktopMode = false; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 7ff887c7d2..266f3845e2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -31,6 +31,7 @@ import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; @@ -61,10 +62,8 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.SystemUiFlagUtils; @@ -82,6 +81,11 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private static final String TAG = "TaskbarStashController"; private static final boolean DEBUG = false; + /** + * Def. value for @param shouldBubblesFollow in + * {@link #updateAndAnimateTransientTaskbar(boolean)} */ + public static boolean SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE = true; + public static final int FLAG_IN_APP = 1 << 0; public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 1; // shade open, ... public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 2; // setup wizard and AllSetActivity @@ -94,6 +98,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,... public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ... public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview + // An internal no-op flag to determine whether we should delay the taskbar background animation + private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12; + public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat // If any of these flags are enabled, isInApp should return true. private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP; @@ -115,7 +122,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // If any of these flags are enabled, the taskbar must be stashed. private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED - | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN; + | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN + | FLAG_STASHED_FOR_BUBBLES; /** * How long to stash/unstash when manually invoked via long press. @@ -150,12 +158,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * How long to delay the icon/stash handle alpha. */ - private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33; + public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33; /** * How long the icon/stash handle alpha animation plays. */ - private static final long TASKBAR_STASH_ALPHA_DURATION = 50; + public static final long TASKBAR_STASH_ALPHA_DURATION = 50; /** * How long to delay the icon/stash handle alpha for the home to app taskbar animation. @@ -245,7 +253,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private Animator mTaskbarBackgroundAlphaAnimator; private long mTaskbarBackgroundDuration; - private boolean mIsGoingHome; + private boolean mUserIsNotGoingHome = false; // Evaluate whether the handle should be stashed private final LongPredicate mIsStashedPredicate = flags -> { @@ -284,18 +292,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } } - /** - * Show Taskbar upon receiving broadcast - */ - public void showTaskbarFromBroadcast() { - // If user is in middle of taskbar education handle go to next step of education - if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) { - mControllers.taskbarEduTooltipController.hide(); - mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); - } - updateAndAnimateTransientTaskbar(false); - } - /** * Initializes the controller */ @@ -338,7 +334,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell // us that we're paused until a bit later. This avoids flickering upon recreating taskbar. updateStateForFlag(FLAG_IN_APP, true); + applyState(/* duration = */ 0); + + // Hide the background while stashed so it doesn't show on fast swipes home + boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() || + (enableScalingRevealHomeAnimation() + && DisplayController.isTransientTaskbar(mActivity) + && isStashed()); + + mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1); + if (mTaskbarSharedState.getTaskbarWasPinned() || !mTaskbarSharedState.taskbarWasStashedAuto) { tryStartTaskbarTimeout(); @@ -389,6 +395,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return mIsStashed; } + /** Sets the hotseat stashed. */ + public void stashHotseat(boolean stash) { + mControllers.uiController.stashHotseat(stash); + } + + /** Instantly un-stashes the hotseat. */ + public void unStashHotseatInstantly() { + mControllers.uiController.unStashHotseatInstantly(); + } + /** * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash). */ @@ -423,6 +439,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return hasAnyFlag(FLAGS_IN_APP); } + /** Returns whether the taskbar is currently in overview screen. */ + public boolean isInOverview() { + return hasAnyFlag(FLAG_IN_OVERVIEW); + } + + /** Returns whether taskbar is hidden for bubbles. */ + public boolean isHiddenForBubbles() { + return hasAnyFlag(FLAG_STASHED_FOR_BUBBLES); + } + /** * Returns the height that taskbar will be touchable. */ @@ -485,9 +511,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION. * If bubble bar exists, it will match taskbars stashing behavior. + * Will not delay taskbar background by default. */ public void updateAndAnimateTransientTaskbar(boolean stash) { - updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true); + updateAndAnimateTransientTaskbar(stash, SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, false); + } + + /** + * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION. + */ + public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) { + updateAndAnimateTransientTaskbar(stash, shouldBubblesFollow, false); } /** @@ -495,28 +529,47 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * * @param stash whether transient taskbar should be stashed. * @param shouldBubblesFollow whether bubbles should match taskbars behavior. + * @param delayTaskbarBackground whether we will delay the taskbar background animation */ - public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) { + public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow, + boolean delayTaskbarBackground) { if (!DisplayController.isTransientTaskbar(mActivity)) { return; } - if ( - stash - && !mControllers.taskbarAutohideSuspendController - .isSuspendedForTransientTaskbarInLauncher() - && mControllers.taskbarAutohideSuspendController - .isTransientTaskbarStashingSuspended()) { + if (stash + && !mControllers.taskbarAutohideSuspendController + .isSuspendedForTransientTaskbarInLauncher() + && mControllers.taskbarAutohideSuspendController + .isTransientTaskbarStashingSuspended()) { // Avoid stashing if autohide is currently suspended. return; } + boolean shouldApplyState = false; + + if (delayTaskbarBackground) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, true); + shouldApplyState = true; + } + if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) { mTaskbarSharedState.taskbarWasStashedAuto = stash; updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash); + shouldApplyState = true; + } + + if (shouldApplyState) { applyState(); } + // Effectively a no-opp to remove the tag. + if (delayTaskbarBackground) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, + false); + mControllers.taskbarStashController.applyState(0); + } + mControllers.bubbleControllers.ifPresent(controllers -> { if (shouldBubblesFollow) { final boolean willStash = mIsStashedPredicate.test(mState); @@ -570,6 +623,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /* isStashed= */ mActivity.isPhoneMode(), placeholderDuration, TRANSITION_UNSTASH_SUW_MANUAL, + /* skipTaskbarBackgroundDelay */ false, /* jankTag= */ "SUW_MANUAL"); animation.addListener(AnimatorListeners.forEndCallback( () -> mControllers.taskbarViewController.setDeferUpdatesForSUW(false))); @@ -579,13 +633,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Create a stash animation and save to {@link #mAnimator}. * - * @param isStashed whether it's a stash animation or an unstash animation - * @param duration duration of the animation - * @param animationType what transition type to play. - * @param jankTag tag to be used in jank monitor trace. + * @param isStashed whether it's a stash animation or an unstash animation + * @param duration duration of the animation + * @param animationType what transition type to play. + * @param shouldDelayBackground whether we should delay the taskbar bg animation + * @param jankTag tag to be used in jank monitor trace. */ private void createAnimToIsStashed(boolean isStashed, long duration, - @StashAnimation int animationType, String jankTag) { + @StashAnimation int animationType, boolean shouldDelayBackground, String jankTag) { if (animationType == TRANSITION_UNSTASH_SUW_MANUAL && isStashed) { // The STASH_ANIMATION_SUW_MANUAL must only be used during an unstash animation. Log.e(TAG, "Illegal arguments:Using TRANSITION_UNSTASH_SUW_MANUAL to stash taskbar"); @@ -623,7 +678,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } if (isTransientTaskbar) { - createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType); + createTransientAnimToIsStashed(mAnimator, isStashed, duration, + shouldDelayBackground, animationType); } else { createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType); } @@ -729,7 +785,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } private void createTransientAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, - @StashAnimation int animationType) { + boolean shouldDelayBackground, @StashAnimation int animationType) { // Target values of the properties this is going to set final float backgroundOffsetTarget = isStashed ? 1 : 0; final float iconAlphaTarget = isStashed ? 0 : 1; @@ -767,7 +823,10 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba backgroundAndHandleAlphaStartDelay, backgroundAndHandleAlphaDuration, LINEAR); - if (enableScalingRevealHomeAnimation() && !isStashed) { + + if (enableScalingRevealHomeAnimation() + && !isStashed + && shouldDelayBackground) { play(as, getTaskbarBackgroundAnimatorWhenNotGoingHome(duration), 0, 0, LINEAR); as.addListener(AnimatorListeners.forEndCallback( @@ -828,17 +887,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private boolean mTaskbarBgAlphaAnimationStarted = false; @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { - if (mIsGoingHome) { - mTaskbarBgAlphaAnimationStarted = true; - } if (mTaskbarBgAlphaAnimationStarted) { return; } if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) { - if (!mIsGoingHome) { + if (mUserIsNotGoingHome) { playTaskbarBackgroundAlphaAnimation(); - setUserIsGoingHome(false); mTaskbarBgAlphaAnimationStarted = true; } } @@ -850,8 +905,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Sets whether the user is going home based on the current gesture. */ - public void setUserIsGoingHome(boolean isGoingHome) { - mIsGoingHome = isGoingHome; + public void setUserIsNotGoingHome(boolean userIsNotGoingHome) { + mUserIsNotGoingHome = userIsNotGoingHome; } /** @@ -940,13 +995,29 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } public void applyState() { - applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION); + applyState(/* postApplyAction = */ null); + } + + /** Applies state and performs action after state is applied. */ + public void applyState(@Nullable Runnable postApplyAction) { + applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION, postApplyAction); } public void applyState(long duration) { + applyState(duration, /* postApplyAction = */ null); + } + + private void applyState(long duration, @Nullable Runnable postApplyAction) { Animator animator = createApplyStateAnimator(duration); if (animator != null) { + if (postApplyAction != null) { + // performs action on animation end + animator.addListener(AnimatorListeners.forEndCallback(postApplyAction)); + } animator.start(); + } else if (postApplyAction != null) { + // animator was not created, just execute the action + postApplyAction.run(); } } @@ -964,6 +1035,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ @Nullable public Animator createApplyStateAnimator(long duration) { + if (mActivity.isPhoneMode()) { + return null; + } return mStatePropertyHolder.createSetStateAnimator(mState, duration); } @@ -1013,7 +1087,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba long startDelay = 0; updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags, - SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE)); + SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE | SYSUI_STATE_DIALOG_SHOWING)); boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW) && hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED) @@ -1058,10 +1132,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } // Do not stash if hardware keyboard is attached, in 3 button nav and desktop windowing mode - DesktopVisibilityController visibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (visibilityController != null && mActivity.isHardwareKeyboard() - && mActivity.isThreeButtonNav() && visibilityController.areDesktopTasksVisible()) { + if (mActivity.isHardwareKeyboard() + && mActivity.isThreeButtonNav() + && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) { return false; } @@ -1116,6 +1189,10 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR, !hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)); } + if (hasAnyFlag(changedFlags, FLAG_IN_OVERVIEW | FLAG_IN_APP)) { + mControllers.runAfterInit(() -> mControllers.taskbarInsetsController + .onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); + } mActivity.applyForciblyShownFlagWhileTransientTaskbarUnstashed(!isStashedInApp()); } @@ -1130,7 +1207,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ public void setUpTaskbarSystemAction(boolean visible) { UI_HELPER_EXECUTOR.execute(() -> { - if (!visible || !DisplayController.isTransientTaskbar(mActivity)) { + if (!visible || !DisplayController.isTransientTaskbar(mActivity) + || mActivity.isPhoneMode()) { mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR); mIsTaskbarSystemActionRegistered = false; return; @@ -1154,6 +1232,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * Clean up on destroy from TaskbarControllers */ public void onDestroy() { + // If the controller is destroyed before the animation finishes, we cancel the animation + // so that we don't finish the CUJ. + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; + } UI_HELPER_EXECUTOR.execute( () -> mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR)); } @@ -1317,8 +1401,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba mIsStashed = isStashed; mLastStartedTransitionType = animationType; + boolean shouldDelayBackground = hasAnyFlag(FLAG_DELAY_TASKBAR_BG_TAG); // This sets mAnimator. - createAnimToIsStashed(mIsStashed, duration, animationType, + createAnimToIsStashed(mIsStashed, duration, animationType, shouldDelayBackground, computeTaskbarJankMonitorTag(changedFlags)); return mAnimator; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java index 5b6fbef4fd..17516f3616 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java @@ -25,7 +25,6 @@ import androidx.core.content.res.ResourcesCompat; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; /** * Utility class that contains the different taskbar thresholds logic. @@ -39,10 +38,6 @@ public class TaskbarThresholdUtils { private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen, int multiplierDimen) { - if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) { - return r.getDimensionPixelSize(thresholdDimen); - } - float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx; float screenPart = (landscapeScreenHeight * SCREEN_UNITS); float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java new file mode 100644 index 0000000000..615db012ec --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java @@ -0,0 +1,135 @@ +/* + * 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.R; +import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; +import com.android.systemui.shared.statusbar.phone.BarTransitions; + +import java.io.PrintWriter; + +/** Manages task bar transitions */ +public class TaskbarTransitions extends BarTransitions implements + TaskbarControllers.LoggableTaskbarController { + + private final TaskbarActivityContext mContext; + + private boolean mWallpaperVisible; + + private boolean mLightsOut; + private boolean mAutoDim; + private View mNavButtons; + private float mDarkIntensity; + + private final NearestTouchFrame mView; + + public TaskbarTransitions(TaskbarActivityContext context, NearestTouchFrame view) { + super(view, R.drawable.nav_background); + + mContext = context; + mView = view; + } + + void init() { + mView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + mNavButtons = mView.findViewById(R.id.end_nav_buttons); + applyLightsOut(false, true); + }); + mNavButtons = mView.findViewById(R.id.end_nav_buttons); + + applyModeBackground(-1, getMode(), false /*animate*/); + applyLightsOut(false /*animate*/, true /*force*/); + if (mContext.isPhoneButtonNavMode()) { + mBarBackground.setOverrideAlpha(1); + } + } + + void setWallpaperVisibility(boolean visible) { + mWallpaperVisible = visible; + applyLightsOut(true, false); + } + + @Override + public void setAutoDim(boolean autoDim) { + // Ensure we aren't in gestural nav if we are triggering auto dim + if (autoDim && !mContext.isPhoneButtonNavMode()) { + return; + } + if (mAutoDim == autoDim) return; + mAutoDim = autoDim; + applyLightsOut(true, false); + } + + @Override + protected void onTransition(int oldMode, int newMode, boolean animate) { + super.onTransition(oldMode, newMode, animate); + applyLightsOut(animate, false /*force*/); + } + + private void applyLightsOut(boolean animate, boolean force) { + // apply to lights out + applyLightsOut(isLightsOut(getMode()), animate, force); + } + + private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) { + if (!force && lightsOut == mLightsOut) return; + + mLightsOut = lightsOut; + if (mNavButtons == null) return; + + // ok, everyone, stop it right there + mNavButtons.animate().cancel(); + + // Bump percentage by 10% if dark. + float darkBump = mDarkIntensity / 10; + final float navButtonsAlpha = lightsOut ? 0.6f + darkBump : 1f; + + if (!animate) { + mNavButtons.setAlpha(navButtonsAlpha); + } else { + final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION; + mNavButtons.animate() + .alpha(navButtonsAlpha) + .setDuration(duration) + .start(); + } + } + + void onDarkIntensityChanged(float darkIntensity) { + mDarkIntensity = darkIntensity; + if (mAutoDim) { + applyLightsOut(false, true); + } + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarTransitions:"); + + pw.println(prefix + "\tmMode=" + getMode()); + pw.println(prefix + "\tmAlwaysOpaque: " + isAlwaysOpaque()); + pw.println(prefix + "\tmWallpaperVisible: " + mWallpaperVisible); + pw.println(prefix + "\tmLightsOut: " + mLightsOut); + pw.println(prefix + "\tmAutoDim: " + mAutoDim); + pw.println(prefix + "\tbg overrideAlpha: " + mBarBackground.getOverrideAlpha()); + pw.println(prefix + "\tbg color: " + mBarBackground.getColor()); + pw.println(prefix + "\tbg frame: " + mBarBackground.getFrame()); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java index 144c0c25c8..5a5d6d0e53 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java @@ -95,7 +95,8 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY); mControllers.bubbleControllers.ifPresent(controllers -> { controllers.bubbleBarViewController.setTranslationYForSwipe(transY); - controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY); + controllers.bubbleStashedHandleViewController.ifPresent( + controller -> controller.setTranslationYForSwipe(transY)); }); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index 593285f062..9c8c2a9fda 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -20,7 +20,6 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; -import static com.android.quickstep.OverviewCommandHelper.TYPE_HIDE; import android.content.Intent; import android.graphics.drawable.BitmapDrawable; @@ -40,11 +39,12 @@ import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.OverviewCommandHelper.CommandType; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TISBindHelper; import com.android.quickstep.views.RecentsView; +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 com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -61,6 +61,8 @@ public class TaskbarUIController { // Initialized in init. protected TaskbarControllers mControllers; + protected boolean mSkipLauncherVisibilityChange; + @CallSuper protected void init(TaskbarControllers taskbarControllers) { mControllers = taskbarControllers; @@ -96,14 +98,7 @@ public class TaskbarUIController { } /** Called when an icon is launched. */ - @CallSuper - public void onTaskbarIconLaunched(ItemInfo item) { - // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately instead of - // waiting for onPause, to reduce potential visual noise during the app open transition. - if (mControllers.taskbarStashController == null) return; - mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); - mControllers.taskbarStashController.applyState(); - } + public void onTaskbarIconLaunched(ItemInfo item) { } public View getRootView() { return mControllers.taskbarActivityContext.getDragLayer(); @@ -174,11 +169,11 @@ public class TaskbarUIController { || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev); } - /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */ - public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) { + /** Checks if the given {@link MotionEvent} is over the bubble bar views. */ + public boolean isEventOverBubbleBarViews(MotionEvent ev) { return mControllers.bubbleControllers.map( bubbleControllers -> - bubbleControllers.bubbleStashController.isEventOverStashHandle(ev)) + bubbleControllers.bubbleStashController.isEventOverBubbleBarViews(ev)) .orElse(false); } @@ -230,7 +225,7 @@ public class TaskbarUIController { } recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback( - Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()), + Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()), false /* findExactPairMatch */, foundTasks -> { @Nullable Task foundTask = foundTasks[0]; @@ -247,6 +242,13 @@ public class TaskbarUIController { * Uses the clicked Taskbar icon to launch a second app for splitscreen. */ public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) { + // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately + // to reduce potential visual noise during the app open transition. + if (mControllers.taskbarStashController != null) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); + mControllers.taskbarStashController.applyState(); + } + RecentsView recents = getRecentsView(); recents.getSplitSelectController().findLastActiveTasksAndRunCallback( Collections.singletonList(info.getComponentKey()), @@ -269,8 +271,8 @@ public class TaskbarUIController { foundTaskView, foundTask, taskContainer.getIconView().getDrawable(), - taskContainer.getThumbnailViewDeprecated(), - taskContainer.getThumbnailViewDeprecated().getThumbnail(), + taskContainer.getSnapshotView(), + taskContainer.getSplitAnimationThumbnail(), null /* intent */, null /* user */, info); @@ -392,7 +394,7 @@ public class TaskbarUIController { if (overviewCommandHelper == null) { return; } - overviewCommandHelper.addCommand(TYPE_HIDE); + overviewCommandHelper.addCommand(CommandType.HIDE); } /** @@ -415,7 +417,29 @@ public class TaskbarUIController { /** * Sets whether the user is going home based on the current gesture. */ - public void setUserIsGoingHome(boolean isGoingHome) { - mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome); + public void setUserIsNotGoingHome(boolean isNotGoingHome) { + mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome); + } + + /** + * Sets whether to prevent taskbar from reacting to launcher visibility during the recents + * transition animation. + */ + public void setSkipLauncherVisibilityChange(boolean skip) { + mSkipLauncherVisibilityChange = skip; + } + + /** Sets whether the hotseat is stashed */ + public void stashHotseat(boolean stash) { + } + + /** Un-stash the hotseat instantly */ + public void unStashHotseatInstantly() { + } + + /** + * Called when we want to unstash taskbar when user performs swipes up gesture. + */ + public void onSwipeToUnstashTaskbar() { } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 570221c872..2734137a29 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -19,17 +19,17 @@ import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_ import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; import static com.android.launcher3.Flags.enableCursorHoverStates; +import static com.android.launcher3.Flags.enableRecentsInTaskbar; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR; import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.AttributeSet; import android.view.DisplayCutout; @@ -37,18 +37,16 @@ import android.view.InputDevice; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; -import androidx.annotation.DimenRes; -import androidx.annotation.DrawableRes; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -60,14 +58,19 @@ import com.android.launcher3.model.data.CollectionInfo; 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.customization.TaskbarAllAppsButtonContainer; +import com.android.launcher3.taskbar.customization.TaskbarDividerContainer; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.IconButtonView; -import com.android.quickstep.DeviceConfigWrapper; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import java.util.List; import java.util.function.Predicate; /** @@ -86,6 +89,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private final boolean mIsRtl; private final TaskbarActivityContext mActivityContext; + @Nullable private BubbleBarLocation mBubbleBarLocation = null; // Initialized in init. private TaskbarViewCallbacks mControllerCallbacks; @@ -93,16 +97,22 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private View.OnLongClickListener mIconLongClickListener; // Only non-null when the corresponding Folder is open. - private @Nullable FolderIcon mLeaveBehindFolderIcon; + @Nullable private FolderIcon mLeaveBehindFolderIcon; // Only non-null when device supports having an All Apps button. - private @Nullable IconButtonView mAllAppsButton; - private Runnable mAllAppsTouchRunnable; - private long mAllAppsButtonTouchDelayMs; - private boolean mAllAppsTouchTriggered; + @Nullable private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer; - // Only non-null when device supports having an All Apps button. - private @Nullable IconButtonView mTaskbarDivider; + // Only non-null when device supports having a Divider button. + @Nullable private TaskbarDividerContainer mTaskbarDividerContainer; + + // Only non-null when device supports having a Taskbar Overflow button. + @Nullable private IconButtonView mTaskbarOverflowView; + + /** + * Whether the divider is between Hotseat icons and Recents, + * instead of between All Apps button and Hotseat. + */ + private boolean mAddedDividerForRecents; private final View mQsb; @@ -160,55 +170,21 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // Needed to draw folder leave-behind when opening one. setWillNotDraw(false); - mAllAppsButton = (IconButtonView) LayoutInflater.from(context) - .inflate(R.layout.taskbar_all_apps_button, this, false); - mAllAppsButton.setIconDrawable(resources.getDrawable( - getAllAppsButton(isTransientTaskbar))); - mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); - mAllAppsButton.setForegroundTint( - mActivityContext.getColor(R.color.all_apps_button_color)); + mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context); - if (enableTaskbarPinning()) { - mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate( - R.layout.taskbar_divider, - this, false); - mTaskbarDivider.setIconDrawable( - resources.getDrawable(R.drawable.taskbar_divider_button)); - mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + if (enableTaskbarPinning() || enableRecentsInTaskbar()) { + mTaskbarDividerContainer = new TaskbarDividerContainer(context); } + if (Flags.taskbarOverflow()) { + mTaskbarOverflowView = (IconButtonView) LayoutInflater.from(context) + .inflate(R.layout.taskbar_overflow_button, this, false); + mTaskbarOverflowView.setIconDrawable( + resources.getDrawable(R.drawable.taskbar_overflow_icon)); + mTaskbarOverflowView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + } // TODO: Disable touch events on QSB otherwise it can crash. mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); - - // Default long press (touch) delay = 400ms - mAllAppsButtonTouchDelayMs = ViewConfiguration.getLongPressTimeout(); - } - - @DrawableRes - private int getAllAppsButton(boolean isTransientTaskbar) { - boolean shouldSelectTransientIcon = - (isTransientTaskbar || enableTaskbarPinning()) - && !mActivityContext.isThreeButtonNav(); - if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { - return shouldSelectTransientIcon - ? R.drawable.ic_transient_taskbar_all_apps_search_button - : R.drawable.ic_taskbar_all_apps_search_button; - } else { - return shouldSelectTransientIcon - ? R.drawable.ic_transient_taskbar_all_apps_button - : R.drawable.ic_taskbar_all_apps_button; - } - } - - @DimenRes - public int getAllAppsButtonTranslationXOffset(boolean isTransientTaskbar) { - if (isTransientTaskbar) { - return R.dimen.transient_taskbar_all_apps_button_translation_x_offset; - } else { - return ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get() - ? R.dimen.taskbar_all_apps_search_button_translation_x_offset - : R.dimen.taskbar_all_apps_button_translation_x_offset; - } } @Override @@ -234,18 +210,40 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar @Override public void onDeviceProfileChanged(DeviceProfile dp) { - mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar; + mShouldTryStartAlign = mActivityContext.shouldStartAlignTaskbar(); } @Override public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) { - announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title)); + announceTaskbarShown(); } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) { - announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title)); + announceTaskbarHidden(); } return super.performAccessibilityActionInternal(action, arguments); + } + private void announceTaskbarShown() { + BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible(); + if (bubbleBarLocation == null) { + announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title)); + } else if (bubbleBarLocation.isOnLeft(isLayoutRtl())) { + announceForAccessibility( + mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_left_title)); + } else { + announceForAccessibility( + mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_right_title)); + } + } + + private void announceTaskbarHidden() { + BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible(); + if (bubbleBarLocation == null) { + announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title)); + } else { + announceForAccessibility( + mContext.getString(R.string.taskbar_a11y_hidden_with_bubbles_title)); + } } protected void announceAccessibilityChanges() { @@ -271,27 +269,17 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconClickListener = mControllerCallbacks.getIconOnClickListener(); mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener(); - if (mAllAppsButton != null) { - mAllAppsButton.setOnClickListener(this::onAllAppsButtonClick); - mAllAppsButton.setOnLongClickListener(this::onAllAppsButtonLongClick); - mAllAppsButton.setOnTouchListener(this::onAllAppsButtonTouch); - mAllAppsButton.setHapticFeedbackEnabled( - mControllerCallbacks.isAllAppsButtonHapticFeedbackEnabled()); - mAllAppsTouchRunnable = () -> { - mControllerCallbacks.triggerAllAppsButtonLongClick(); - mAllAppsTouchTriggered = true; - }; - AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(mContext); - if (DeviceConfigWrapper.get().getCustomLpaaThresholds() - && assistStateManager.getLPNHDurationMillis().isPresent()) { - mAllAppsButtonTouchDelayMs = assistStateManager.getLPNHDurationMillis().get(); - } + if (mAllAppsButtonContainer != null) { + mAllAppsButtonContainer.setUpCallbacks(callbacks); } - if (mTaskbarDivider != null && !mActivityContext.isThreeButtonNav()) { - mTaskbarDivider.setOnLongClickListener( - mControllerCallbacks.getTaskbarDividerLongClickListener()); - mTaskbarDivider.setOnTouchListener( - mControllerCallbacks.getTaskbarDividerRightClickListener()); + if (mTaskbarDividerContainer != null && callbacks.supportsDividerLongPress()) { + mTaskbarDividerContainer.setUpCallbacks(callbacks); + } + if (mTaskbarOverflowView != null) { + mTaskbarOverflowView.setOnClickListener( + mControllerCallbacks.getOverflowOnClickListener()); + mTaskbarOverflowView.setOnLongClickListener( + mControllerCallbacks.getOverflowOnLongClickListener()); } } @@ -308,21 +296,25 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar /** * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos. */ - protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) { + protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List recentTasks) { int nextViewIndex = 0; int numViewsAnimated = 0; + mAddedDividerForRecents = false; - if (mAllAppsButton != null) { - removeView(mAllAppsButton); + if (mAllAppsButtonContainer != null) { + removeView(mAllAppsButtonContainer); - if (mTaskbarDivider != null) { - removeView(mTaskbarDivider); + if (mTaskbarDividerContainer != null) { + removeView(mTaskbarDividerContainer); } } + if (mTaskbarOverflowView != null) { + removeView(mTaskbarOverflowView); + } removeView(mQsb); - for (int i = 0; i < hotseatItemInfos.length; i++) { - ItemInfo hotseatItemInfo = hotseatItemInfos[i]; + // Add Hotseat icons. + for (ItemInfo hotseatItemInfo : hotseatItemInfos) { if (hotseatItemInfo == null) { continue; } @@ -388,11 +380,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index. - if (hotseatView instanceof BubbleTextView - && hotseatItemInfo instanceof WorkspaceItemInfo) { - BubbleTextView btv = (BubbleTextView) hotseatView; - WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo; - + if (hotseatView instanceof BubbleTextView btv + && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) { boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo); btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated); if (animate) { @@ -405,17 +394,83 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } nextViewIndex++; } + + if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { + addView(mTaskbarDividerContainer, nextViewIndex++); + mAddedDividerForRecents = true; + if (mTaskbarOverflowView != null) { + addView(mTaskbarOverflowView, nextViewIndex++); + } + } + + // Add Recent/Running icons. + for (GroupTask task : recentTasks) { + // Replace any Recent views with the appropriate type if it's not already that type. + final int expectedLayoutResId; + boolean isCollection = false; + if (task.hasMultipleTasks()) { + if (task instanceof DesktopTask) { + // TODO(b/316004172): use Desktop tile layout. + expectedLayoutResId = -1; + } else { + // TODO(b/343289567): use R.layout.app_pair_icon + expectedLayoutResId = -1; + } + isCollection = true; + } else { + expectedLayoutResId = R.layout.taskbar_app_icon; + } + + View recentIcon = null; + while (nextViewIndex < getChildCount()) { + recentIcon = getChildAt(nextViewIndex); + + // see if the view can be reused + if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId) + || (isCollection && (recentIcon.getTag() != task))) { + removeAndRecycle(recentIcon); + recentIcon = null; + } else { + // View found + break; + } + } + + if (recentIcon == null) { + if (isCollection) { + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + continue; + } + + recentIcon = inflate(expectedLayoutResId); + LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); + recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + addView(recentIcon, nextViewIndex, lp); + } + + if (recentIcon instanceof BubbleTextView btv) { + applyGroupTaskToBubbleTextView(btv, task); + } + setClickAndLongClickListenersForIcon(recentIcon); + if (enableCursorHoverStates()) { + setHoverListenerForIcon(recentIcon); + } + nextViewIndex++; + } + // Remove remaining views while (nextViewIndex < getChildCount()) { removeAndRecycle(getChildAt(nextViewIndex)); } - if (mAllAppsButton != null) { - addView(mAllAppsButton, mIsRtl ? getChildCount() : 0); + if (mAllAppsButtonContainer != null) { + addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0); - // if only all apps button present, don't include divider view. - if (mTaskbarDivider != null && getChildCount() > 1) { - addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1); + // If there are no recent tasks, add divider after All Apps (unless it's the only view). + if (!mAddedDividerForRecents + && mTaskbarDividerContainer != null + && getChildCount() > 1) { + addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1); } } if (mActivityContext.getDeviceProfile().isQsbInline) { @@ -425,6 +480,20 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } + /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */ + public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) { + // TODO(b/343289567): support app pairs. + Task task1 = groupTask.task1; + // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state + // while dragging. + Drawable taskIcon = groupTask.task1.icon; + if (taskIcon != null) { + taskIcon = taskIcon.getConstantState().newDrawable().mutate(); + } + btv.applyIconAndLabel(taskIcon, task1.titleDescription); + btv.setTag(groupTask); + } + /** * Sets OnClickListener and OnLongClickListener for the given view. */ @@ -450,39 +519,60 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon)); } + /** Updates taskbar icons accordingly to the new bubble bar location. */ + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + if (mBubbleBarLocation == location) return; + mBubbleBarLocation = location; + requestLayout(); + } + + /** + * Returns translation X for the taskbar icons for provided {@link BubbleBarLocation}. If the + * bubble bar is not enabled, or location of the bubble bar is the same, or taskbar is not start + * aligned - returns 0. + */ + public float getTranslationXForBubbleBarPosition(BubbleBarLocation location) { + if (!mControllerCallbacks.isBubbleBarEnabledInPersistentTaskbar() + || location == mBubbleBarLocation + || !mActivityContext.shouldStartAlignTaskbar() + ) { + return 0; + } + Rect iconsBounds = getIconLayoutBounds(); + return getTaskBarIconsEndForBubbleBarLocation(location) - iconsBounds.right; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int count = getChildCount(); - DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int spaceNeeded = getIconLayoutWidth(); - int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; boolean layoutRtl = isLayoutRtl(); - int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2; - int iconEnd; - + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; + int centerAlignIconEnd = (right + left + spaceNeeded) / 2; + int iconEnd = centerAlignIconEnd; if (mShouldTryStartAlign) { - // Taskbar is aligned to the start int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx; - - if (layoutRtl) { - iconEnd = right - startSpacingPx; + if (mControllerCallbacks.isBubbleBarEnabledInPersistentTaskbar() + && mBubbleBarLocation != null + && mActivityContext.shouldStartAlignTaskbar()) { + iconEnd = (int) getTaskBarIconsEndForBubbleBarLocation(mBubbleBarLocation); } else { - iconEnd = startSpacingPx + spaceNeeded; + if (layoutRtl) { + iconEnd = right - startSpacingPx; + } else { + iconEnd = startSpacingPx + spaceNeeded; + } + boolean needMoreSpaceForNav = layoutRtl + ? navSpaceNeeded > (iconEnd - spaceNeeded) + : iconEnd > (right - navSpaceNeeded); + if (needMoreSpaceForNav) { + // Add offset to account for nav bar when taskbar is centered + int offset = layoutRtl + ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded) + : (right - navSpaceNeeded) - centerAlignIconEnd; + iconEnd = centerAlignIconEnd + offset; + } } - } else { - iconEnd = centerAlignIconEnd; - } - - boolean needMoreSpaceForNav = layoutRtl - ? navSpaceNeeded > (iconEnd - spaceNeeded) - : iconEnd > (right - navSpaceNeeded); - if (needMoreSpaceForNav) { - // Add offset to account for nav bar when taskbar is centered - int offset = layoutRtl - ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded) - : (right - navSpaceNeeded) - centerAlignIconEnd; - - iconEnd = centerAlignIconEnd + offset; } // Currently, we support only one device with display cutout and we only are concern about @@ -514,6 +604,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconLayoutBounds.right = iconEnd; mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2; mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize; + int count = getChildCount(); for (int i = count; i > 0; i--) { View child = getChildAt(i - 1); if (child == mQsb) { @@ -529,7 +620,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2; int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight; child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom); - } else if (child == mTaskbarDivider) { + } else if (child == mTaskbarDividerContainer) { iconEnd += mItemMarginLeftRight; int iconStart = iconEnd - mIconTouchSize; child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); @@ -567,8 +658,20 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates); } + /** + * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch + * area, rather than layout placement in the parent view. + */ + public Rect getIconLayoutVisualBounds() { + return new Rect(mIconLayoutBounds); + } + + /** Gets taskbar layout bounds in parent view. */ public Rect getIconLayoutBounds() { - return mIconLayoutBounds; + Rect actualBounds = new Rect(mIconLayoutBounds); + actualBounds.top = getTop(); + actualBounds.bottom = getBottom(); + return actualBounds; } /** @@ -607,16 +710,32 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar * Returns the all apps button in the taskbar. */ @Nullable - public View getAllAppsButtonView() { - return mAllAppsButton; + public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() { + return mAllAppsButtonContainer; } /** * Returns the taskbar divider in the taskbar. */ @Nullable - public View getTaskbarDividerView() { - return mTaskbarDivider; + public TaskbarDividerContainer getTaskbarDividerViewContainer() { + return mTaskbarDividerContainer; + } + + /** + * Returns the taskbar overflow view in the taskbar. + */ + @Nullable + public IconButtonView getTaskbarOverflowView() { + return mTaskbarOverflowView; + } + + /** + * Returns whether the divider is between Hotseat icons and Recents, + * instead of between All Apps button and Hotseat. + */ + public boolean isDividerForRecents() { + return mAddedDividerForRecents; } /** @@ -677,7 +796,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // map over all the shortcuts on the taskbar for (int i = 0; i < getChildCount(); i++) { View item = getChildAt(i); - if (op.evaluate((ItemInfo) item.getTag(), item)) { + // TODO(b/344657629): Support GroupTask as well for notification dots/popup + if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) { return; } } @@ -694,6 +814,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar View item = getChildAt(i); if (!(item.getTag() instanceof ItemInfo)) { // Should only happen for All Apps button. + // Will also happen for Recent/Running app icons. (Which have GroupTask as tags) continue; } ItemInfo info = (ItemInfo) item.getTag(); @@ -702,48 +823,21 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } } - return mAllAppsButton; + return mAllAppsButtonContainer; } - private boolean onAllAppsButtonTouch(View view, MotionEvent ev) { - switch (ev.getAction()) { - case MotionEvent.ACTION_DOWN: - mAllAppsTouchTriggered = false; - MAIN_EXECUTOR.getHandler().postDelayed( - mAllAppsTouchRunnable, mAllAppsButtonTouchDelayMs); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - cancelAllAppsButtonTouch(); + /** + * This method only works for bubble bar enabled in persistent task bar and the taskbar is start + * aligned. + */ + private float getTaskBarIconsEndForBubbleBarLocation(BubbleBarLocation location) { + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + boolean navbarOnRight = location.isOnLeft(isLayoutRtl()); + int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; + if (navbarOnRight) { + return getWidth() - navSpaceNeeded; + } else { + return navSpaceNeeded + getIconLayoutWidth(); } - return false; - } - - private void cancelAllAppsButtonTouch() { - MAIN_EXECUTOR.getHandler().removeCallbacks(mAllAppsTouchRunnable); - // ACTION_UP is first triggered, then click listener / long-click listener is triggered on - // the next frame, so we need to post twice and delay the reset. - if (mAllAppsButton != null) { - mAllAppsButton.post(() -> { - mAllAppsButton.post(() -> { - mAllAppsTouchTriggered = false; - }); - }); - } - } - - private void onAllAppsButtonClick(View view) { - if (!mAllAppsTouchTriggered) { - mControllerCallbacks.triggerAllAppsButtonClick(view); - } - } - - // Handle long click from Switch Access and Voice Access - private boolean onAllAppsButtonLongClick(View view) { - if (!MAIN_EXECUTOR.getHandler().hasCallbacks(mAllAppsTouchRunnable) - && !mAllAppsTouchTriggered) { - mControllerCallbacks.triggerAllAppsButtonLongClick(); - } - return true; } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java index 3c646cb7fa..d108d8c357 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java @@ -23,8 +23,13 @@ import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.Nullable; + import com.android.internal.jank.Cuj; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import com.android.wm.shell.Flags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Callbacks for {@link TaskbarView} to interact with its controller. @@ -47,7 +52,7 @@ public class TaskbarViewCallbacks { } /** Trigger All Apps button click action. */ - protected void triggerAllAppsButtonClick(View v) { + public void triggerAllAppsButtonClick(View v) { InteractionJankMonitorWrapper.begin(v, Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS, /* tag= */ "TASKBAR_BUTTON"); mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP); @@ -55,7 +60,7 @@ public class TaskbarViewCallbacks { } /** Trigger All Apps button long click action. */ - protected void triggerAllAppsButtonLongClick() { + public void triggerAllAppsButtonLongClick() { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS); } @@ -70,6 +75,11 @@ public class TaskbarViewCallbacks { }; } + /** Check to see if we support long press on taskbar divider */ + public boolean supportsDividerLongPress() { + return !mActivity.isThreeButtonNav(); + } + public View.OnTouchListener getTaskbarDividerRightClickListener() { return (v, event) -> { if (event.isFromSource(InputDevice.SOURCE_MOUSE) @@ -104,4 +114,45 @@ public class TaskbarViewCallbacks { mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged( mTaskbarView.getVisibility()); } + + /** + * Get current location of bubble bar, if it is visible. + * Returns {@code null} if bubble bar is not shown. + */ + @Nullable + public BubbleBarLocation getBubbleBarLocationIfVisible() { + BubbleBarViewController bubbleBarViewController = + mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null); + if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) { + return bubbleBarViewController.getBubbleBarLocation(); + } + return null; + } + + /** Returns true if bubble bar controllers present and enabled in persistent taskbar. */ + public boolean isBubbleBarEnabledInPersistentTaskbar() { + return Flags.enableBubbleBarInPersistentTaskBar() + && mControllers.bubbleControllers.isPresent(); + } + + /** Returns on click listener for the taskbar overflow view. */ + public View.OnClickListener getOverflowOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + mControllers.keyboardQuickSwitchController.openQuickSwitchView(); + } + }; + } + + /** Returns on long click listener for the taskbar overflow view. */ + public View.OnLongClickListener getOverflowOnLongClickListener() { + return new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + mControllers.keyboardQuickSwitchController.openQuickSwitchView(); + return true; + } + }; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 55745b557a..b207b37f56 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -29,7 +29,10 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UN import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_NAV_BAR_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM; @@ -46,6 +49,7 @@ import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.OneShotPreDrawListener; import com.android.app.animation.Interpolators; @@ -63,13 +67,18 @@ import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.TaskItemInfo; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; -import com.android.launcher3.views.IconButtonView; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.Flags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.Set; @@ -78,7 +87,8 @@ import java.util.function.Predicate; /** * Handles properties/data collection, then passes the results to TaskbarView to render. */ -public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController { +public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController, + BubbleBarController.BubbleBarLocationListener { private static final String TAG = "TaskbarViewController"; @@ -91,7 +101,17 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4; public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5; public static final int ALPHA_INDEX_SMALL_SCREEN = 6; - private static final int NUM_ALPHA_CHANNELS = 7; + + public static final int ALPHA_INDEX_BUBBLE_BAR = 7; + private static final int NUM_ALPHA_CHANNELS = 8; + + /** Only used for animation purposes, to position the divider between two item indices. */ + public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f; + + /** Used if an unexpected edge case is hit in {@link #getPositionInHotseat}. */ + private static final float ERROR_POSITION_IN_HOTSEAT_NOT_FOUND = -100; + + private static boolean sEnableModelLoadingForTests = true; private final TaskbarActivityContext mActivity; private final TaskbarView mTaskbarView; @@ -108,13 +128,17 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar private final AnimatedFloat mTaskbarIconTranslationXForPinning = new AnimatedFloat( this::updateTaskbarIconTranslationXForPinning); + private final AnimatedFloat mIconsTranslationXForNavbar = new AnimatedFloat( + this::updateTranslationXForNavBar); + + @Nullable + private Animator mTaskbarShiftXAnim; + @Nullable + private BubbleBarLocation mCurrentBubbleBarLocation; + private final AnimatedFloat mTaskbarIconTranslationYForPinning = new AnimatedFloat( this::updateTranslationY); - private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) - -> updateTaskbarIconTranslationXForPinning(); - private AnimatedFloat mTaskbarNavButtonTranslationY; private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay; @@ -129,12 +153,19 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar // Initialized in init. private TaskbarControllers mControllers; + private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + updateTaskbarIconTranslationXForPinning(); + mControllers.navbarButtonsViewController.onTaskbarLayoutChange(); + }; + // Animation to align icons with Launcher, created lazily. This allows the controller to be // active only during the animation and does not need to worry about layout changes. private AnimatorPlaybackController mIconAlignControllerLazy = null; private Runnable mOnControllerPreCreateCallback = NO_OP; // Stored here as signals to determine if the mIconAlignController needs to be recreated. + private boolean mIsIconAlignedWithHotseat; private boolean mIsHotseatIconOnTopWhenAligned; private boolean mIsStashed; @@ -189,9 +220,10 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mTaskbarIconTranslationXForPinning.updateValue(pinningValue); mModelCallbacks.init(controllers); - if (mActivity.isUserSetupComplete()) { + if (mActivity.isUserSetupComplete() && sEnableModelLoadingForTests) { // Only load the callbacks if user setup is completed - LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks); + controllers.runAfterInit(() -> LauncherAppState.getInstance(mActivity).getModel() + .addCallbacksAndLoad(mModelCallbacks)); } mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); @@ -203,14 +235,62 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { // This gets modified in NavbarButtonsViewController, but the initial value it reads // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here - mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN) - .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start(); + mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue( + mActivity.isPhoneMode() ? 0 : 1); } if (enableTaskbarPinning()) { mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener); } } + /** Adjusts start aligned taskbar layout accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + updateCurrentBubbleBarLocation(location); + if (!shouldMoveTaskbarOnBubbleBarLocationUpdate()) return; + cancelTaskbarShiftAnimation(); + // reset translation x, taskbar will position icons with the updated location + mIconsTranslationXForNavbar.updateValue(0); + mTaskbarView.onBubbleBarLocationUpdated(location); + } + + /** Animates start aligned taskbar accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + if (!updateCurrentBubbleBarLocation(location) + || !shouldMoveTaskbarOnBubbleBarLocationUpdate()) { + return; + } + cancelTaskbarShiftAnimation(); + float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location); + mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX); + mTaskbarShiftXAnim.start(); + } + + /** Updates the mCurrentBubbleBarLocation, returns {@code} true if location is updated. */ + private boolean updateCurrentBubbleBarLocation(BubbleBarLocation location) { + if (mCurrentBubbleBarLocation == location || location == null) { + return false; + } else { + mCurrentBubbleBarLocation = location; + return true; + } + } + + /** Returns whether taskbar should be moved on the bubble bar location update. */ + private boolean shouldMoveTaskbarOnBubbleBarLocationUpdate() { + return Flags.enableBubbleBarInPersistentTaskBar() + && mControllers.bubbleControllers.isPresent() + && mActivity.shouldStartAlignTaskbar() + && mActivity.isThreeButtonNav(); + } + + private void cancelTaskbarShiftAnimation() { + if (mTaskbarShiftXAnim != null) { + mTaskbarShiftXAnim.cancel(); + } + } + /** * Announcement for Accessibility when Taskbar stashes/unstashes. */ @@ -224,7 +304,13 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener); - mModelCallbacks.unregisterListeners(); + } + + /** + * Gets the taskbar {@link View.Visibility visibility}. + */ + public int getTaskbarVisibility() { + return mTaskbarView.getVisibility(); } public boolean areIconsVisible() { @@ -259,6 +345,10 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar OneShotPreDrawListener.add(mTaskbarView, listener); } + public Rect getIconLayoutVisualBounds() { + return mTaskbarView.getIconLayoutVisualBounds(); + } + public Rect getIconLayoutBounds() { return mTaskbarView.getIconLayoutBounds(); } @@ -273,7 +363,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar @Nullable public View getAllAppsButtonView() { - return mTaskbarView.getAllAppsButtonView(); + return mTaskbarView.getAllAppsButtonContainer(); } public AnimatedFloat getTaskbarIconScaleForStash() { @@ -340,9 +430,9 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar View[] iconViews = mTaskbarView.getIconViews(); float scale = mTaskbarIconTranslationXForPinning.value; float transientTaskbarAllAppsOffset = mActivity.getResources().getDimension( - mTaskbarView.getAllAppsButtonTranslationXOffset(true)); + mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(true)); float persistentTaskbarAllAppsOffset = mActivity.getResources().getDimension( - mTaskbarView.getAllAppsButtonTranslationXOffset(false)); + mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(false)); float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset, persistentTaskbarAllAppsOffset); @@ -357,7 +447,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } if (mActivity.isThreeButtonNav()) { - ((IconButtonView) mTaskbarView.getAllAppsButtonView()) + mTaskbarView.getAllAppsButtonContainer() .setTranslationXForTaskbarAllAppsIcon(allAppIconTranslateRange); return; } @@ -382,8 +472,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar -finalMarginScale * (iconIndex - halfIconCount)); } - if (iconView.equals(mTaskbarView.getAllAppsButtonView())) { - ((IconButtonView) iconView).setTranslationXForTaskbarAllAppsIcon( + if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) { + mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon( allAppIconTranslateRange); } } @@ -434,6 +524,17 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar + mTaskbarIconTranslationYForSpringOnStash); } + private void updateTranslationXForNavBar() { + View[] iconViews = mTaskbarView.getIconViews(); + float translationX = mIconsTranslationXForNavbar.value; + for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) { + View iconView = iconViews[iconIndex]; + MultiTranslateDelegate translateDelegate = + ((Reorderable) iconView).getTranslateDelegate(); + translateDelegate.getTranslationX(INDEX_NAV_BAR_ANIM).setValue(translationX); + } + } + /** * Computes translation y for taskbar pinning. */ @@ -449,14 +550,14 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar if (mControllers.getSharedState().startTaskbarVariantIsTransient) { float transY = mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight - - mTaskbarView.getIconLayoutBounds().bottom) + - mTaskbarView.getIconLayoutVisualBounds().bottom) - (mPersistentTaskbarDp.taskbarHeight - mTransientTaskbarDp.taskbarIconSize) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY); } else { float transY = -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight - - mTaskbarView.getIconLayoutBounds().bottom) + - mTaskbarView.getIconLayoutVisualBounds().bottom) - (mTransientTaskbarDp.taskbarHeight - mTransientTaskbarDp.taskbarIconSize) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f); @@ -511,30 +612,43 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } public View getTaskbarDividerView() { - return mTaskbarView.getTaskbarDividerView(); + return mTaskbarView.getTaskbarDividerViewContainer(); } - /** Updates which icons are marked as running given the Set of currently running packages. */ - public void updateIconViewsRunningStates(Set runningPackages, - Set minimizedPackages) { + /** + * Updates which icons are marked as running or minimized given the Sets of currently running + * and minimized tasks. + */ + public void updateIconViewsRunningStates(Set runningTaskIds, + Set minimizedTaskIds) { for (View iconView : getIconViews()) { if (iconView instanceof BubbleTextView btv) { btv.updateRunningState( - getRunningAppState(btv.getTargetPackageName(), runningPackages, - minimizedPackages)); + getRunningAppState(btv, runningTaskIds, minimizedTaskIds)); } } } private BubbleTextView.RunningAppState getRunningAppState( - String packageName, - Set runningPackages, - Set minimizedPackages) { - if (minimizedPackages.contains(packageName)) { - return BubbleTextView.RunningAppState.MINIMIZED; + BubbleTextView btv, + Set runningTaskIds, + Set minimizedTaskIds) { + Object tag = btv.getTag(); + if (tag instanceof TaskItemInfo itemInfo) { + if (minimizedTaskIds.contains(itemInfo.getTaskId())) { + return BubbleTextView.RunningAppState.MINIMIZED; + } + if (runningTaskIds.contains(itemInfo.getTaskId())) { + return BubbleTextView.RunningAppState.RUNNING; + } } - if (runningPackages.contains(packageName)) { - return BubbleTextView.RunningAppState.RUNNING; + if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) { + if (minimizedTaskIds.contains(groupTask.task1.key.id)) { + return BubbleTextView.RunningAppState.MINIMIZED; + } + if (runningTaskIds.contains(groupTask.task1.key.id)) { + return BubbleTextView.RunningAppState.RUNNING; + } } return BubbleTextView.RunningAppState.NOT_RUNNING; } @@ -635,15 +749,17 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mIconAlignControllerLazy = null; return; } - boolean isHotseatIconOnTopWhenAligned = mControllers.uiController.isHotseatIconOnTopWhenAligned(); + boolean isIconAlignedWithHotseat = mControllers.uiController.isIconAlignedWithHotseat(); boolean isStashed = mControllers.taskbarStashController.isStashed(); - // Re-create animation when mIsHotseatIconOnTopWhenAligned or mIsStashed changes. + // Re-create animation when any of these values change. if (mIconAlignControllerLazy == null || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned + || mIsIconAlignedWithHotseat != isIconAlignedWithHotseat || mIsStashed != isStashed) { mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned; + mIsIconAlignedWithHotseat = isIconAlignedWithHotseat; mIsStashed = isStashed; mIconAlignControllerLazy = createIconAlignmentController(launcherDp); } @@ -695,17 +811,24 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar ? mTransientTaskbarDp.taskbarBottomMargin : mPersistentTaskbarDp.taskbarBottomMargin; + int firstRecentTaskIndex = -1; for (int i = 0; i < mTaskbarView.getChildCount(); i++) { View child = mTaskbarView.getChildAt(i); - boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView(); - boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerView(); + boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonContainer(); + boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerViewContainer(); + boolean isTaskbarOverflowView = child == mTaskbarView.getTaskbarOverflowView(); + boolean isRecentTask = child.getTag() instanceof GroupTask; + // TODO(b/343522351): show recents on the home screen. + final boolean isRecentsInHotseat = false; if (!mIsHotseatIconOnTopWhenAligned) { // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController // plays iconAlignment to 1 really fast, therefore moving the fading towards the end // to avoid icons disappearing rather than fading out visually. setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f)); - } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) - || (isTaskbarDividerView && enableTaskbarPinning())) { + } else if ((isAllAppsButton && !FeatureFlags.enableAllAppsButtonInHotseat()) + || (isTaskbarDividerView && enableTaskbarPinning()) + || (isRecentTask && !isRecentsInHotseat) + || isTaskbarOverflowView) { if (!isToHome && mIsHotseatIconOnTopWhenAligned && mIsStashed) { @@ -766,25 +889,17 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar continue; } - float positionInHotseat; - if (isAllAppsButton) { - // Note that there is no All Apps button in the hotseat, - // this position is only used as its convenient for animation purposes. - positionInHotseat = Utilities.isRtl(child.getResources()) - ? taskbarDp.numShownHotseatIcons - : -1; - } else if (isTaskbarDividerView) { - // Note that there is no taskbar divider view in the hotseat, - // this position is only used as its convenient for animation purposes. - positionInHotseat = Utilities.isRtl(child.getResources()) - ? taskbarDp.numShownHotseatIcons - 0.5f - : -0.5f; - } else if (child.getTag() instanceof ItemInfo) { - positionInHotseat = ((ItemInfo) child.getTag()).screenId; - } else { - Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); - continue; + int recentTaskIndex = -1; + if (isRecentTask) { + if (firstRecentTaskIndex < 0) { + firstRecentTaskIndex = i; + } + recentTaskIndex = i - firstRecentTaskIndex; } + float positionInHotseat = getPositionInHotseat(taskbarDp.numShownHotseatIcons, child, + mIsRtl, isAllAppsButton, isTaskbarDividerView, + mTaskbarView.isDividerForRecents(), recentTaskIndex); + if (positionInHotseat == ERROR_POSITION_IN_HOTSEAT_NOT_FOUND) continue; float hotseatAdjustedBorderSpace = launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext()); @@ -820,6 +935,58 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return controller; } + /** + * Returns the index of the given child relative to its position in hotseat. + * Examples: + * -1 is the item before the first hotseat item. + * -0.5 is between those (e.g. for the divider). + * {@link #ERROR_POSITION_IN_HOTSEAT_NOT_FOUND} if there's no calculation relative to hotseat. + */ + @VisibleForTesting + float getPositionInHotseat(int numShownHotseatIcons, View child, boolean isRtl, + boolean isAllAppsButton, boolean isTaskbarDividerView, boolean isDividerForRecents, + int recentTaskIndex) { + float positionInHotseat; + // Note that there is no All Apps button in the hotseat, + // this position is only used as it's convenient for animation purposes. + float allAppsButtonPositionInHotseat = isRtl + // Right after all hotseat items. + // [HHHHHH]|[>A<] + ? numShownHotseatIcons + // Right before all hotseat items. + // [>A<]|[HHHHHH] + : -1; + // Note that there are no recent tasks in the hotseat, + // this position is only used as it's convenient for animation purposes. + float firstRecentTaskPositionInHotseat = isRtl + // After all hotseat icons and All Apps button. + // [HHHHHH][A]|[>RR 0 + ? relativePosition - DIVIDER_VIEW_POSITION_OFFSET + : relativePosition + DIVIDER_VIEW_POSITION_OFFSET; + } else if (child.getTag() instanceof ItemInfo) { + positionInHotseat = ((ItemInfo) child.getTag()).screenId; + } else if (recentTaskIndex >= 0) { + positionInHotseat = firstRecentTaskPositionInHotseat + recentTaskIndex; + } else { + Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); + return ERROR_POSITION_IN_HOTSEAT_NOT_FOUND; + } + return positionInHotseat; + } + private boolean bubbleBarHasBubbles() { return mControllers.bubbleControllers.isPresent() && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles(); @@ -869,6 +1036,27 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return mTaskbarView.isEventOverAnyItem(ev); } + /** Called when there's a change in running apps to update the UI. */ + public void commitRunningAppsToUI() { + mModelCallbacks.commitRunningAppsToUI(); + } + + /** + * To be called when the given Task is updated, so that we can tell TaskbarView to also update. + * @param task The Task whose e.g. icon changed. + */ + public void onTaskUpdated(Task task) { + // Find the icon view(s) that changed. + for (View view : mTaskbarView.getIconViews()) { + if (view instanceof BubbleTextView btv + && view.getTag() instanceof GroupTask groupTask) { + if (groupTask.containsTask(task.key.id)) { + mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask); + } + } + } + } + @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarViewController:"); @@ -883,20 +1071,22 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar "ALPHA_INDEX_RECENTS_DISABLED", "ALPHA_INDEX_NOTIFICATION_EXPANDED", "ALPHA_INDEX_ASSISTANT_INVOKED", - "ALPHA_INDEX_IME_BUTTON_NAV", "ALPHA_INDEX_SMALL_SCREEN"); mModelCallbacks.dumpLogs(prefix + "\t", pw); } - /** Called when there's a change in running apps to update the UI. */ - public void commitRunningAppsToUI() { - mModelCallbacks.commitRunningAppsToUI(); + /** Enables model loading for tests. */ + @VisibleForTesting + public static void enableModelLoadingForTests(boolean enable) { + sEnableModelLoadingForTests = enable; } - /** Call TaskbarModelCallbacks to update running apps. */ - public void updateRunningApps() { - mModelCallbacks.updateRunningApps(); + private ObjectAnimator createTaskbarIconsShiftAnimator(float translationX) { + ObjectAnimator animator = mIconsTranslationXForNavbar.animateToValue(translationX); + animator.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS); + animator.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); + animator.setInterpolator(Interpolators.EMPHASIZED); + return animator; } - } diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt index 5a5ff8e880..c380c8d6f1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt @@ -41,7 +41,8 @@ private const val TEMP_BACKGROUND_WINDOW_TITLE = "VoiceInteractionTaskbarBackgro class VoiceInteractionWindowController(val context: TaskbarActivityContext) : TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController { - private val isSeparateBackgroundEnabled = !DisplayController.isTransientTaskbar(context) + private val isSeparateBackgroundEnabled = + !DisplayController.isTransientTaskbar(context) && !context.isPhoneMode private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context) private val nonTouchableInsetsComputer = ViewTreeObserver.OnComputeInternalInsetsListener { @@ -109,7 +110,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : } fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) { - if (isVoiceInteractionWindowVisible == visible) { + if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) { return } isVoiceInteractionWindowVisible = visible diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java index 5d91acd3e1..a46845affa 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java @@ -17,13 +17,10 @@ package com.android.launcher3.taskbar.allapps; import android.content.Context; import android.util.AttributeSet; -import android.view.View; import androidx.annotation.Nullable; -import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import java.util.Optional; @@ -46,23 +43,6 @@ public class TaskbarAllAppsContainerView extends mOnInvalidateHeaderListener = onInvalidateHeaderListener; } - @Override - protected View inflateSearchBar() { - if (isSearchSupported()) { - return super.inflateSearchBar(); - } - - // Remove top padding of header, since we do not have any search - mHeader.setPadding(mHeader.getPaddingLeft(), 0, - mHeader.getPaddingRight(), mHeader.getPaddingBottom()); - - TaskbarAllAppsFallbackSearchContainer searchView = - new TaskbarAllAppsFallbackSearchContainer(getContext(), null); - searchView.setId(R.id.search_container_all_apps); - searchView.setVisibility(GONE); - return searchView; - } - @Override public void invalidateHeader() { super.invalidateHeader(); @@ -70,11 +50,6 @@ public class TaskbarAllAppsContainerView extends OnInvalidateHeaderListener::onInvalidateHeader); } - @Override - protected boolean isSearchSupported() { - return FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get(); - } - @Override public boolean isInAllApps() { // All apps is always open diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java deleted file mode 100644 index 53fe06d32c..0000000000 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.launcher3.taskbar.allapps; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.android.launcher3.ExtendedEditText; -import com.android.launcher3.allapps.ActivityAllAppsContainerView; -import com.android.launcher3.allapps.SearchUiManager; - -/** Empty search container for Taskbar All Apps used as a fallback if search is not supported. */ -public class TaskbarAllAppsFallbackSearchContainer extends View implements SearchUiManager { - public TaskbarAllAppsFallbackSearchContainer(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TaskbarAllAppsFallbackSearchContainer( - Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public void initializeSearch(ActivityAllAppsContainerView containerView) { - // Do nothing. - } - - @Override - public void resetSearch() { - // Do nothing. - } - - @Nullable - @Override - public ExtendedEditText getEditText() { - return null; - } -} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java index 90ac87202b..5830095fa6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java @@ -16,7 +16,6 @@ package com.android.launcher3.taskbar.allapps; import static com.android.app.animation.Interpolators.EMPHASIZED; -import static com.android.launcher3.Flags.enablePredictiveBackGesture; import static com.android.launcher3.touch.AllAppsSwipeController.ALL_APPS_FADE_MANUAL; import static com.android.launcher3.touch.AllAppsSwipeController.SCRIM_FADE_MANUAL; @@ -40,7 +39,6 @@ import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import com.android.launcher3.util.Themes; @@ -181,9 +179,7 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView mBubbleStashedHandleViewController; private BubblePinController mBubblePinController; + private BubbleCreator mBubbleCreator; + private BubbleBarLocationListener mBubbleBarLocationListener; // Cache last sent top coordinate to avoid sending duplicate updates to shell private int mLastSentBubbleBarTop; @@ -167,6 +139,8 @@ public class BubbleBarController extends IBubblesListener.Stub { List removedBubbles; List bubbleKeysInOrder; Point expandedViewDropTargetSize; + boolean showOverflow; + boolean showOverflowChanged; // These need to be loaded in the background BubbleBarBubble addedBubble; @@ -185,6 +159,8 @@ public class BubbleBarController extends IBubblesListener.Stub { removedBubbles = update.removedBubbles; bubbleKeysInOrder = update.bubbleKeysInOrder; expandedViewDropTargetSize = update.expandedViewDropTargetSize; + showOverflow = update.showOverflow; + showOverflowChanged = update.showOverflowChanged; } } @@ -193,17 +169,6 @@ public class BubbleBarController extends IBubblesListener.Stub { mBarView = bubbleView; // Need the view for inflating bubble views. mSystemUiProxy = SystemUiProxy.INSTANCE.get(context); - - if (sBubbleBarEnabled) { - mSystemUiProxy.setBubblesListener(this); - } - mLauncherApps = context.getSystemService(LauncherApps.class); - mIconFactory = new BubbleIconFactory(context, - context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size), - context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size), - context.getResources().getColor(R.color.important_conversation), - context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.importance_ring_stroke_width)); } public void onDestroy() { @@ -212,43 +177,30 @@ public class BubbleBarController extends IBubblesListener.Stub { /** Initializes controllers. */ public void init(BubbleControllers bubbleControllers, + BubbleBarLocationListener bubbleBarLocationListener, ImeVisibilityChecker imeVisibilityChecker) { mImeVisibilityChecker = imeVisibilityChecker; mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController; mBubblePinController = bubbleControllers.bubblePinController; + mBubbleCreator = bubbleControllers.bubbleCreator; + mBubbleBarLocationListener = bubbleBarLocationListener; bubbleControllers.runAfterInit(() -> { mBubbleBarViewController.setHiddenForBubbles( !sBubbleBarEnabled || mBubbles.isEmpty()); - mBubbleStashedHandleViewController.setHiddenForBubbles( - !sBubbleBarEnabled || mBubbles.isEmpty()); + mBubbleStashedHandleViewController.ifPresent( + controller -> controller.setHiddenForBubbles( + !sBubbleBarEnabled || mBubbles.isEmpty())); mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse( key -> setSelectedBubbleInternal(mBubbles.get(key))); mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged); - }); - } - /** - * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet. - * - *

This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating - * the overflow multiple times. - */ - private void createAndAddOverflowIfNeeded() { - if (mOverflowBubble == null) { - BubbleBarOverflow overflow = createOverflow(mContext); - MAIN_EXECUTOR.execute(() -> { - // we're on the main executor now, so check that the overflow hasn't been created - // again to avoid races. - if (mOverflowBubble == null) { - mBubbleBarViewController.addBubble( - overflow, /* isExpanding= */ false, /* suppressAnimation= */ true); - mOverflowBubble = overflow; - } - }); - } + if (sBubbleBarEnabled) { + mSystemUiProxy.setBubblesListener(this); + } + }); } /** @@ -259,10 +211,11 @@ public class BubbleBarController extends IBubblesListener.Stub { mBubbleBarViewController.setHiddenForSysui(hideBubbleBar); boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0; - mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView); + mBubbleStashedHandleViewController.ifPresent( + controller -> controller.setHiddenForSysui(hideHandleView)); boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0; - mBubbleStashController.onSysuiLockedStateChange(sysuiLocked); + mBubbleStashController.setSysuiLocked(sysuiLocked); } // @@ -280,23 +233,25 @@ public class BubbleBarController extends IBubblesListener.Stub { || !update.currentBubbleList.isEmpty()) { // We have bubbles to load BUBBLE_STATE_EXECUTOR.execute(() -> { - createAndAddOverflowIfNeeded(); if (update.addedBubble != null) { - viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView, + viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext, + update.addedBubble, + mBarView, null /* existingBubble */); } if (update.updatedBubble != null) { BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey()); viewUpdate.updatedBubble = - populateBubble(mContext, update.updatedBubble, mBarView, + mBubbleCreator.populateBubble(mContext, update.updatedBubble, + mBarView, existingBubble); } if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { List currentBubbles = new ArrayList<>(); for (int i = 0; i < update.currentBubbleList.size(); i++) { - BubbleBarBubble b = - populateBubble(mContext, update.currentBubbleList.get(i), mBarView, - null /* existingBubble */); + BubbleBarBubble b = mBubbleCreator.populateBubble(mContext, + update.currentBubbleList.get(i), mBarView, + null /* existingBubble */); currentBubbles.add(b); } viewUpdate.currentBubbles = currentBubbles; @@ -322,27 +277,70 @@ public class BubbleBarController extends IBubblesListener.Stub { || mImeVisibilityChecker.isImeVisible(); BubbleBarBubble bubbleToSelect = null; - if (!update.removedBubbles.isEmpty()) { - for (int i = 0; i < update.removedBubbles.size(); i++) { - RemovedBubble removedBubble = update.removedBubbles.get(i); - BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); - if (bubble != null) { - mBubbleBarViewController.removeBubble(bubble); - } else { - Log.w(TAG, "trying to remove bubble that doesn't exist: " - + removedBubble.getKey()); + + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null + && update.removedBubbles.isEmpty()) { + // A bubble was added from the overflow (& now it's empty / not showing) + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble); + } else if (update.addedBubble != null && update.removedBubbles.size() == 1) { + // we're adding and removing a bubble at the same time. handle this as a single update. + RemovedBubble removedBubble = update.removedBubbles.get(0); + BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey()); + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + boolean showOverflow = update.showOverflowChanged && update.showOverflow; + if (bubbleToRemove != null) { + mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble, + bubbleToRemove, isExpanding, suppressAnimation, showOverflow); + } else { + mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, + suppressAnimation); + Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey()); + } + } else { + boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && update.showOverflow; + if (!update.removedBubbles.isEmpty()) { + for (int i = 0; i < update.removedBubbles.size(); i++) { + RemovedBubble removedBubble = update.removedBubbles.get(i); + BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); + if (bubble != null && overflowNeedsToBeAdded) { + // First removal, show the overflow + overflowNeedsToBeAdded = false; + mBubbleBarViewController.addOverflowAndRemoveBubble(bubble); + } else if (bubble != null) { + mBubbleBarViewController.removeBubble(bubble); + } else { + Log.w(TAG, "trying to remove bubble that doesn't exist: " + + removedBubble.getKey()); + } } } - } - if (update.addedBubble != null) { - mBubbles.put(update.addedBubble.getKey(), update.addedBubble); - mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation); - if (isCollapsed) { - // If we're collapsed, the most recently added bubble will be selected. - bubbleToSelect = update.addedBubble; + if (update.addedBubble != null) { + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, + suppressAnimation); + } + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged + && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) { + mBubbleBarViewController.showOverflow(update.showOverflow); } - } + + // if a bubble was updated upstream, but removed before the update was received, add it back + if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) { + mBubbles.put(update.updatedBubble.getKey(), update.updatedBubble); + mBubbleBarViewController.addBubble( + update.updatedBubble, isExpanding, suppressAnimation); + } + + if (update.addedBubble != null && isCollapsed) { + // If we're collapsed, the most recently added bubble will be selected. + bubbleToSelect = update.addedBubble; + } + if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) { // Iterate in reverse because new bubbles are added in front and the list is in order. for (int i = update.currentBubbles.size() - 1; i >= 0; i--) { @@ -360,10 +358,14 @@ public class BubbleBarController extends IBubblesListener.Stub { } } } + if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) { + mBubbleBarViewController.showOverflow(true); + } // Adds and removals have happened, update visibility before any other visual changes. mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); - mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty()); + mBubbleStashedHandleViewController.ifPresent( + controller -> controller.setHiddenForBubbles(mBubbles.isEmpty())); if (mBubbles.isEmpty()) { // all bubbles were removed. clear the selected bubble @@ -376,7 +378,8 @@ public class BubbleBarController extends IBubblesListener.Stub { BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey()); // If we're not stashed, we're visible so animate bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */); - mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false); + mBubbleBarViewController.animateBubbleNotification( + bb, /* isExpanding= */ false, /* isUpdate= */ true); } if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) { // Create the new list @@ -427,18 +430,16 @@ public class BubbleBarController extends IBubblesListener.Stub { } } + /** + * Removes the given bubble from the backing list of bubbles after it was dismissed by the user. + */ + public void onBubbleDismissed(BubbleView bubble) { + mBubbles.remove(bubble.getBubble().getKey()); + } + /** Tells WMShell to show the currently selected bubble. */ public void showSelectedBubble() { if (getSelectedBubbleKey() != null) { - if (mSelectedBubble instanceof BubbleBarBubble) { - // Because we've visited this bubble, we should suppress the notification. - // This is updated on WMShell side when we show the bubble, but that update isn't - // passed to launcher, instead we apply it directly here. - BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo(); - info.setFlags( - info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - mSelectedBubble.getView().updateDotVisibility(true /* animate */); - } mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen(); mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop); } else { @@ -490,145 +491,27 @@ public class BubbleBarController extends IBubblesListener.Stub { private void updateBubbleBarLocationInternal(BubbleBarLocation location) { mBubbleBarViewController.setBubbleBarLocation(location); mBubbleStashController.setBubbleBarLocation(location); + mBubbleBarLocationListener.onBubbleBarLocationUpdated(location); } @Override public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { MAIN_EXECUTOR.execute( - () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation)); + () -> { + mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation); + mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation); + }); + } + + /** Notifies WMShell to show the expanded view. */ + void showExpandedView() { + mSystemUiProxy.showExpandedView(); } // // Loading data for the bubbles // - @Nullable - private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv, - @Nullable BubbleBarBubble existingBubble) { - String appName; - Bitmap badgeBitmap; - Bitmap bubbleBitmap; - Path dotPath; - int dotColor; - - boolean isImportantConvo = b.isImportantConversation(); - - ShortcutRequest.QueryResult result = new ShortcutRequest(context, - new UserHandle(b.getUserId())) - .forPackage(b.getPackageName(), b.getShortcutId()) - .query(FLAG_MATCH_DYNAMIC - | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER - | FLAG_MATCH_CACHED - | FLAG_GET_PERSONS_DATA); - - ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null; - if (shortcutInfo == null) { - Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey() - + " with shortcutId: " + b.getShortcutId()); - } - - ApplicationInfo appInfo; - try { - appInfo = mLauncherApps.getApplicationInfo( - b.getPackageName(), - 0, - new UserHandle(b.getUserId())); - } catch (PackageManager.NameNotFoundException e) { - // If we can't find package... don't think we should show the bubble. - Log.w(TAG, "Unable to find packageName: " + b.getPackageName()); - return null; - } - if (appInfo == null) { - Log.w(TAG, "Unable to find appInfo: " + b.getPackageName()); - return null; - } - PackageManager pm = context.getPackageManager(); - appName = String.valueOf(appInfo.loadLabel(pm)); - Drawable appIcon = appInfo.loadUnbadgedIcon(pm); - Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId())); - - // Badged bubble image - Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo, - b.getIcon()); - if (bubbleDrawable == null) { - // Default to app icon - bubbleDrawable = appIcon; - } - - BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo); - badgeBitmap = badgeBitmapInfo.icon; - - float[] bubbleBitmapScale = new float[1]; - bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); - - // Dot color & placement - Path iconPath = PathParser.createPathFromPathData( - context.getResources().getString( - com.android.internal.R.string.config_icon_mask)); - Matrix matrix = new Matrix(); - float scale = bubbleBitmapScale[0]; - float radius = BubbleView.DEFAULT_PATH_SIZE / 2f; - matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, - radius /* pivot y */); - iconPath.transform(matrix); - dotPath = iconPath; - dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, - Color.WHITE, WHITE_SCRIM_ALPHA / 255f); - - if (existingBubble == null) { - LayoutInflater inflater = LayoutInflater.from(context); - BubbleView bubbleView = (BubbleView) inflater.inflate( - R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); - - BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, - badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); - bubbleView.setBubble(bubble); - return bubble; - } else { - // If we already have a bubble (so it already has an inflated view), update it. - existingBubble.setInfo(b); - existingBubble.setBadge(badgeBitmap); - existingBubble.setIcon(bubbleBitmap); - existingBubble.setDotColor(dotColor); - existingBubble.setDotPath(dotPath); - existingBubble.setAppName(appName); - return existingBubble; - } - } - - private BubbleBarOverflow createOverflow(Context context) { - Bitmap bitmap = createOverflowBitmap(context); - LayoutInflater inflater = LayoutInflater.from(context); - BubbleView bubbleView = (BubbleView) inflater.inflate( - R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */); - BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView); - bubbleView.setOverflow(overflow, bitmap); - return overflow; - } - - private Bitmap createOverflowBitmap(Context context) { - Drawable iconDrawable = AppCompatResources.getDrawable(mContext, - R.drawable.bubble_ic_overflow_button); - - final TypedArray ta = mContext.obtainStyledAttributes( - new int[]{ - com.android.internal.R.attr.materialColorOnPrimaryFixed, - com.android.internal.R.attr.materialColorPrimaryFixed - }); - int overflowIconColor = ta.getColor(0, Color.WHITE); - int overflowBackgroundColor = ta.getColor(1, Color.BLACK); - ta.recycle(); - - iconDrawable.setTint(overflowIconColor); - - int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset); - Drawable foreground = new InsetDrawable(iconDrawable, inset); - Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor), - foreground); - - return mIconFactory.createBadgedIconBitmap(drawable).icon; - } - private void onBubbleBarBoundsChanged() { int newTop = mBarView.getRestingTopPositionOnScreen(); if (newTop != mLastSentBubbleBarTop) { @@ -642,4 +525,14 @@ public class BubbleBarController extends IBubblesListener.Stub { /** Whether the IME is visible. */ boolean isImeVisible(); } + + /** Listener of {@link BubbleBarLocation} updates. */ + public interface BubbleBarLocationListener { + + /** Called when {@link BubbleBarLocation} is animated, but change is not yet final. */ + void onBubbleBarLocationAnimated(BubbleBarLocation location); + + /** Called when {@link BubbleBarLocation} is updated permanently. */ + void onBubbleBarLocationUpdated(BubbleBarLocation location); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt index 43e21f4085..7a32ef152a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt @@ -17,7 +17,7 @@ package com.android.launcher3.taskbar.bubbles import android.graphics.Bitmap import android.graphics.Path -import com.android.wm.shell.common.bubbles.BubbleInfo +import com.android.wm.shell.shared.bubbles.BubbleInfo /** An entity in the bubble bar. */ sealed class BubbleBarItem(open var key: String, open var view: BubbleView) @@ -34,4 +34,8 @@ data class BubbleBarBubble( ) : BubbleBarItem(info.key, view) /** Represents the overflow bubble in the bubble bar. */ -data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view) +data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) { + companion object { + const val KEY = "Overflow" + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt new file mode 100644 index 0000000000..8e176ac6bd --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.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 + +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** Composite implementation of [BubbleBarLocationListener] interface */ +class BubbleBarLocationCompositeListener(private val listeners: List) : + BubbleBarLocationListener { + + constructor(vararg listeners: BubbleBarLocationListener) : this(listOf(*listeners)) + + override fun onBubbleBarLocationAnimated(location: BubbleBarLocation?) { + listeners.forEach { it.onBubbleBarLocationAnimated(location) } + } + + override fun onBubbleBarLocationUpdated(location: BubbleBarLocation?) { + listeners.forEach { it.onBubbleBarLocationUpdated(location) } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt index 9e5ffc9dc4..a34fab2b8f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt @@ -27,8 +27,10 @@ import android.view.View import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar @@ -41,12 +43,17 @@ class BubbleBarPinController( private lateinit var bubbleBarViewController: BubbleBarViewController private lateinit var bubbleStashController: BubbleStashController + private lateinit var bubbleBarLocationListener: BubbleBarLocationListener private var exclRectWidth: Float = 0f private var exclRectHeight: Float = 0f private var dropTargetView: View? = null - fun init(bubbleControllers: BubbleControllers) { + fun init( + bubbleControllers: BubbleControllers, + bubbleBarLocationListener: BubbleBarLocationListener + ) { + this.bubbleBarLocationListener = bubbleBarLocationListener bubbleBarViewController = bubbleControllers.bubbleBarViewController bubbleStashController = bubbleControllers.bubbleStashController exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width) @@ -85,6 +92,7 @@ class BubbleBarPinController( val bounds = bubbleBarViewController.bubbleBarBounds val horizontalMargin = bubbleBarViewController.horizontalMargin + bubbleBarLocationListener.onBubbleBarLocationAnimated(location) dropTargetView?.updateLayoutParams { width = bounds.width() height = bounds.height() diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index c7c63e8f86..d454fd7b8e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -30,6 +30,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.LayoutDirection; @@ -38,16 +39,19 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import androidx.dynamicanimation.animation.SpringForce; import com.android.launcher3.R; import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator; import com.android.launcher3.util.DisplayController; -import com.android.wm.shell.Flags; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -91,18 +95,16 @@ public class BubbleBarView extends FrameLayout { private static final long FADE_OUT_ANIM_ALPHA_DURATION_MS = 50L; private static final long FADE_OUT_ANIM_ALPHA_DELAY_MS = 50L; - private static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; + public static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; // During fade out animation we shift the bubble bar 1/80th of the screen width private static final float FADE_OUT_ANIM_POSITION_SHIFT = 1 / 80f; - private static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; + public static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants private static final float FADE_IN_ANIM_POSITION_SPRING_STIFFNESS = 400f; // During fade in animation we shift the bubble bar 1/60th of the screen width private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f; - private static final int SCALE_IN_ANIMATION_DURATION_MS = 250; - /** * Custom property to set alpha value for the bar view while a bubble is being dragged. * Skips applying alpha to the dragged bubble. @@ -122,8 +124,6 @@ public class BubbleBarView extends FrameLayout { private final BubbleBarBackground mBubbleBarBackground; - private boolean mIsAnimatingNewBubble = false; - /** * The current bounds of all the bubble bar. Note that these bounds may not account for * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which @@ -161,11 +161,12 @@ public class BubbleBarView extends FrameLayout { // collapsed state and 1 to the fully expanded state. private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1); - /** An animator used for scaling in a new bubble to the bubble bar while expanded. */ + /** An animator used for animating individual bubbles in the bubble bar while expanded. */ @Nullable - private ValueAnimator mNewBubbleScaleInAnimator = null; + private BubbleAnimator mBubbleAnimator = null; @Nullable private ValueAnimator mScalePaddingAnimator; + @Nullable private Animator mBubbleBarLocationAnimator = null; @@ -182,8 +183,13 @@ public class BubbleBarView extends FrameLayout { @Nullable private BubbleView mDraggedBubbleView; + @Nullable + private BubbleView mDismissedByDragBubbleView; private float mAlphaDuringDrag = 1f; + /** Additional translation in the y direction that is applied to each bubble */ + private float mBubbleOffsetY; + private Controller mController; private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED; @@ -202,7 +208,6 @@ public class BubbleBarView extends FrameLayout { public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setAlpha(0); setVisibility(INVISIBLE); mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); @@ -237,7 +242,11 @@ public class BubbleBarView extends FrameLayout { BubbleView firstBubble = (BubbleView) getChildAt(0); mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); } - updateWidth(); + // If the bar was just expanded, remove the dot from the selected bubble. + if (mIsBarExpanded && mSelectedBubbleView != null) { + mSelectedBubbleView.markSeen(); + } + updateLayoutParams(); }, /* onUpdate= */ animator -> { updateBubblesLayoutProperties(mBubbleBarLocation); @@ -256,9 +265,6 @@ public class BubbleBarView extends FrameLayout { if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { return; } - if (!Flags.animateBubbleSizeChange()) { - setIconSizeAndPadding(newIconSize, newBubbleBarPadding); - } if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) { mScalePaddingAnimator.cancel(); } @@ -301,6 +307,46 @@ public class BubbleBarView extends FrameLayout { } } + /** + * Set scale for bubble bar background in x direction + */ + public void setBackgroundScaleX(float scaleX) { + mBubbleBarBackground.setScaleX(scaleX); + } + + /** + * Set scale for bubble bar background in y direction + */ + public void setBackgroundScaleY(float scaleY) { + mBubbleBarBackground.setScaleY(scaleY); + } + + /** + * Set alpha for bubble views + */ + public void setBubbleAlpha(float alpha) { + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setAlpha(alpha); + } + } + + /** + * Set alpha for bar background + */ + public void setBackgroundAlpha(float alpha) { + mBubbleBarBackground.setAlpha((int) (255 * alpha)); + } + + /** + * Sets offset of each bubble view in the y direction from the base position in the bar. + */ + public void setBubbleOffsetY(float offsetY) { + mBubbleOffsetY = offsetY; + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setTranslationY(getBubbleTranslationY()); + } + } + /** * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders. * @@ -318,7 +364,7 @@ public class BubbleBarView extends FrameLayout { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); - childView.setScaleY(mIconScale); + childView.setScaleX(mIconScale); childView.setScaleY(mIconScale); FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams(); params.height = (int) mIconSize; @@ -363,6 +409,47 @@ public class BubbleBarView extends FrameLayout { } } + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + // Always show only expand action as the menu is only for collapsed bubble bar + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all, + getResources().getString(R.string.bubble_bar_action_dismiss_all))); + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right, + getResources().getString(R.string.bubble_bar_action_move_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left, + getResources().getString(R.string.bubble_bar_action_move_left))); + } + } + + @Override + public boolean performAccessibilityActionInternal(int action, + @androidx.annotation.Nullable Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_EXPAND) { + mController.expandBubbleBar(); + return true; + } + if (action == R.id.action_dismiss_all) { + mController.dismissBubbleBar(); + return true; + } + if (action == R.id.action_move_left) { + mController.updateBubbleBarLocation(BubbleBarLocation.LEFT); + return true; + } + if (action == R.id.action_move_right) { + mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + return true; + } + return false; + } + @SuppressLint("RtlHardcoded") private void onBubbleBarLocationChanged() { final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); @@ -371,6 +458,7 @@ public class BubbleBarView extends FrameLayout { LayoutParams lp = (LayoutParams) getLayoutParams(); lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT); setLayoutParams(lp); // triggers a relayout + updateBubbleAccessibilityStates(); } /** @@ -601,7 +689,9 @@ public class BubbleBarView extends FrameLayout { } setAlphaDuringBubbleDrag(1f); setTranslationX(0f); - setAlpha(1f); + if (mIsBarExpanded && getBubbleChildCount() > 0) { + setAlpha(1f); + } } /** @@ -613,18 +703,31 @@ public class BubbleBarView extends FrameLayout { return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY(); } - /** - * Updates the bounds with translation that may have been applied and returns the result. - */ + /** Returns the bounds with translation that may have been applied. */ public Rect getBubbleBarBounds() { - mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize; - mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY(); - return mBubbleBarBounds; + Rect bounds = new Rect(mBubbleBarBounds); + bounds.top = getTop() + (int) getTranslationY() + mPointerSize; + bounds.bottom = getBottom() + (int) getTranslationY(); + return bounds; + } + + /** Returns the expanded bounds with translation that may have been applied. */ + public Rect getBubbleBarExpandedBounds() { + Rect expandedBounds = getBubbleBarBounds(); + if (!isExpanded() || isExpanding()) { + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + expandedBounds.right = expandedBounds.left + (int) expandedWidth(); + } else { + expandedBounds.left = expandedBounds.right - (int) expandedWidth(); + } + } + return expandedBounds; } /** * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height * respectively. If the value is not in range of 0 to 1 it will be normalized. + * * @param x relative X pivot value in range 0..1 * @param y relative Y pivot value in range 0..1 */ @@ -653,69 +756,174 @@ public class BubbleBarView extends FrameLayout { return mRelativePivotY; } - /** Notifies the bubble bar that a new bubble animation is starting. */ - public void onAnimatingBubbleStarted() { - mIsAnimatingNewBubble = true; - } - - /** Notifies the bubble bar that a new bubble animation is complete. */ - public void onAnimatingBubbleCompleted() { - mIsAnimatingNewBubble = false; - } - /** Add a new bubble to the bubble bar. */ - public void addBubble(View bubble, FrameLayout.LayoutParams lp) { + public void addBubble(BubbleView bubble) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, + Gravity.LEFT); + final int index = bubble.isOverflow() ? getChildCount() : 0; + if (isExpanded()) { // if we're expanded scale the new bubble in bubble.setScaleX(0f); bubble.setScaleY(0f); - addView(bubble, 0, lp); - createNewBubbleScaleInAnimator(bubble); - mNewBubbleScaleInAnimator.start(); + addView(bubble, index, lp); + bubble.showDotIfNeeded(/* animate= */ false); + + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + updateLayoutParams(); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(1); + bubble.setScaleY(1); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + bubble.setScaleX(animatedFraction); + bubble.setScaleY(animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener); } else { - addView(bubble, 0, lp); + addView(bubble, index, lp); } } - private void createNewBubbleScaleInAnimator(View bubble) { - mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1); - mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS); - mNewBubbleScaleInAnimator.addUpdateListener(animation -> { - float animatedFraction = animation.getAnimatedFraction(); - bubble.setScaleX(animatedFraction); - bubble.setScaleY(animatedFraction); - updateBubblesLayoutProperties(mBubbleBarLocation); - invalidate(); - }); - mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() { + /** Add a new bubble and remove an old bubble from the bubble bar. */ + public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble, + Runnable onEndRunnable) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, + Gravity.LEFT); + boolean isOverflowSelected = mSelectedBubbleView.isOverflow(); + boolean removingOverflow = removedBubble.isOverflow(); + boolean addingOverflow = addedBubble.isOverflow(); + + if (!isExpanded()) { + removeView(removedBubble); + int index = addingOverflow ? getChildCount() : 0; + addView(addedBubble, index, lp); + if (onEndRunnable != null) { + onEndRunnable.run(); + } + return; + } + int index = addingOverflow ? getChildCount() : 0; + addedBubble.setScaleX(0f); + addedBubble.setScaleY(0f); + addView(addedBubble, index, lp); + + if (isOverflowSelected && removingOverflow) { + // The added bubble will be selected + mSelectedBubbleView = addedBubble; + } + int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView); + int indexOfBubbleToRemove = indexOfChild(removedBubble); + + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + @Override - public void onAnimationCancel(Animator animation) { - bubble.setScaleX(1); - bubble.setScaleY(1); + public void onAnimationEnd() { + removeView(removedBubble); + updateLayoutParams(); + mBubbleAnimator = null; + if (onEndRunnable != null) { + onEndRunnable.run(); + } } @Override - public void onAnimationEnd(Animator animation) { - updateWidth(); - mNewBubbleScaleInAnimator = null; + public void onAnimationCancel() { + addedBubble.setScaleX(1); + addedBubble.setScaleY(1); + removedBubble.setScaleX(0); + removedBubble.setScaleY(0); } - }); + + @Override + public void onAnimationUpdate(float animatedFraction) { + addedBubble.setScaleX(animatedFraction); + addedBubble.setScaleY(animatedFraction); + removedBubble.setScaleX(1 - animatedFraction); + removedBubble.setScaleY(1 - animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove, + listener); } - // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { - if (getChildCount() + 1 > MAX_BUBBLES) { - // the last child view is the overflow bubble and we shouldn't remove that. remove the - // second to last child view. - removeViewInLayout(getChildAt(getChildCount() - 2)); - } super.addView(child, index, params); - updateWidth(); + updateLayoutParams(); updateBubbleAccessibilityStates(); updateContentDescription(); } + /** Removes the given bubble from the bubble bar. */ + public void removeBubble(View bubble) { + if (isExpanded()) { + // TODO b/347062801 - animate the bubble bar if the last bubble is removed + final boolean dismissedByDrag = mDraggedBubbleView == bubble; + if (dismissedByDrag) { + mDismissedByDragBubbleView = mDraggedBubbleView; + } + int bubbleCount = getChildCount(); + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + removeView(bubble); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(0); + bubble.setScaleY(0); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + // don't update the scale if this bubble was dismissed by drag + if (!dismissedByDrag) { + bubble.setScaleX(1 - animatedFraction); + bubble.setScaleY(1 - animatedFraction); + } + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + int bubbleIndex = indexOfChild(bubble); + BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1); + String lastBubbleKey = lastBubble.getBubble().getKey(); + boolean removingLastBubble = + BubbleBarOverflow.KEY.equals(lastBubbleKey) + ? bubbleIndex == bubbleCount - 2 + : bubbleIndex == bubbleCount - 1; + mBubbleAnimator.animateRemovedBubble( + indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble, + listener); + } else { + removeView(bubble); + } + } + // TODO: (b/283309949) animate it @Override public void removeView(View view) { @@ -724,15 +932,54 @@ public class BubbleBarView extends FrameLayout { mSelectedBubbleView = null; mBubbleBarBackground.showArrow(false); } - updateWidth(); + updateLayoutParams(); updateBubbleAccessibilityStates(); updateContentDescription(); + mDismissedByDragBubbleView = null; + updateNotificationDotsIfCollapsed(); } - private void updateWidth() { - LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); - setLayoutParams(lp); + /** + * Return child views in the order which they are shown on the screen. + *

+ * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index + * 0. + * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index + * 0.
+ * + * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the + * left, the most recent bubble is shown on the right. The bubbles from the example above would + * be shown as: (3)(2)(1).
+ * + * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the + * example above would be shown as: (1)(2)(3). + */ + private List getChildViewsInOnScreenOrder() { + List childViews = new ArrayList<>(getChildCount()); + for (int i = 0; i < getChildCount(); i++) { + childViews.add(getChildAt(i)); + } + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + // Visually child views are shown in reverse order when bar is on the left + return childViews.reversed(); + } + return childViews; + } + + private void updateNotificationDotsIfCollapsed() { + if (isExpanded()) { + return; + } + for (int i = 0; i < getChildCount(); i++) { + BubbleView bubbleView = (BubbleView) getChildAt(i); + // when we're collapsed, the first bubble should show the dot if it has it. the rest of + // the bubbles should hide their dots. + if (i == 0 && bubbleView.hasUnseenContent()) { + bubbleView.showDotIfNeeded(/* animate= */ true); + } else { + bubbleView.hideDot(); + } + } } private void updateLayoutParams() { @@ -762,18 +1009,14 @@ public class BubbleBarView extends FrameLayout { final float currentWidth = getWidth(); final float expandedWidth = expandedWidth(); final float collapsedWidth = collapsedWidth(); - int bubbleCount = getChildCount(); - float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); - float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); - // When translating X & Y the scale is ignored, so need to deduct it from the translations - final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); - final boolean animate = getVisibility() == VISIBLE; + int childCount = getChildCount(); + final float ty = getBubbleTranslationY(); final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); // elevation state is opposite to widthState - when expanded all icons are flat float elevationState = (1 - widthState); - for (int i = 0; i < bubbleCount; i++) { + for (int i = 0; i < childCount; i++) { BubbleView bv = (BubbleView) getChildAt(i); - if (bv == mDraggedBubbleView) { + if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) { // Skip the dragged bubble. Its translation is managed by the drag controller. continue; } @@ -781,44 +1024,52 @@ public class BubbleBarView extends FrameLayout { bv.setDragTranslationX(0f); bv.setOffsetX(0f); - bv.setScaleX(mIconScale); - bv.setScaleY(mIconScale); + if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) { + // if the bubble animator is running don't set scale here, it will be set by the + // animator + bv.setScaleX(mIconScale); + bv.setScaleY(mIconScale); + } bv.setTranslationY(ty); + // the position of the bubble when the bar is fully expanded - final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft); + final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft); // the position of the bubble when the bar is fully collapsed - final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft); + final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft); // slowly animate elevation while keeping correct Z ordering float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i; bv.setZ(fullElevationForChild * elevationState); + // only update the dot and badge scale if we're expanding or collapsing + if (mWidthAnimator.isRunning()) { + // The dot for the selected bubble scales in the opposite direction of the expansion + // animation. + bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState); + // The badge for the selected bubble is always at full scale. All other bubbles + // scale according to the expand animation. + bv.setBadgeScale(bv == mSelectedBubbleView ? 1 : widthState); + } + if (mIsBarExpanded) { // If bar is on the right, account for bubble bar expanding and shifting left final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth; // where the bubble will end up when the animation ends final float targetX = expandedX + expandedBarShift; bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); - // When we're expanded, we're not stacked so we're not behind the stack - bv.setBehindStack(false, animate); - bv.setAlpha(1); + bv.setVisibility(VISIBLE); } else { // If bar is on the right, account for bubble bar expanding and shifting left final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth; final float targetX = collapsedX + collapsedBarShift; bv.setTranslationX(widthState * (expandedX - targetX) + targetX); - // If we're not the first bubble we're behind the stack - bv.setBehindStack(i > 0, animate); - // If we're fully collapsed, hide all bubbles except for the first 2. If there are - // only 2 bubbles, hide the second bubble as well because it's the overflow. + // If we're fully collapsed, hide all bubbles except for the first 2, excluding + // the overflow. if (widthState == 0) { - if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { - bv.setAlpha(0); - } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1 - && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) { - bv.setAlpha(0); + if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { + bv.setVisibility(INVISIBLE); } else { - bv.setAlpha(1); + bv.setVisibility(VISIBLE); } } } @@ -861,9 +1112,8 @@ public class BubbleBarView extends FrameLayout { } final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; float translationX; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - translationX = getExpandedBubbleTranslationXDuringScaleAnimation( - bubbleIndex, bubbleCount, onLeft); + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getBubbleTranslationX(bubbleIndex) + mBubbleBarPadding; } else if (onLeft) { translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing; } else { @@ -872,66 +1122,36 @@ public class BubbleBarView extends FrameLayout { return translationX - getScaleIconShift(); } - /** - * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is - * expanded and a new bubble is animating in. - * - *

This method assumes that the animation is running so callers are expected to verify that - * before calling it. - */ - private float getExpandedBubbleTranslationXDuringScaleAnimation( - int bubbleIndex, int bubbleCount, boolean onLeft) { - // when the new bubble scale animation is running, a new bubble is animating in while the - // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded - // one, and the new one animating in. - - if (mNewBubbleScaleInAnimator == null) { - // callers of this method are expected to verify that the animation is running, but the - // compiler doesn't know that. - return 0; - } - final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - // the new bubble is scaling in from the center, so we need to adjust its translation so - // that the distance to the adjacent bubble scales at the same rate. - final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f; - - if (onLeft) { - if (bubbleIndex == 0) { - // this is the animating bubble. use scaled spacing between it and the bubble to - // its left - return (bubbleCount - 1) * getScaledIconSize() - + (bubbleCount - 2) * mExpandedBarIconsSpacing - + newBubbleScale * mExpandedBarIconsSpacing - + pivotAdjustment; - } - // when the bubble bar is on the left, only the translation of the right-most bubble - // is affected by the scale animation. - return (bubbleCount - bubbleIndex - 1) * iconAndSpacing; - } else if (bubbleIndex == 0) { - // the bubble bar is on the right, and this is the animating bubble. it only needs - // to be adjusted for the scaling pivot. - return pivotAdjustment; - } else { - return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale); - } - } - - private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, - boolean onLeft) { - if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { + private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) { + if (bubbleIndex < 0 || bubbleIndex >= childCount) { return 0; } float translationX; if (onLeft) { - // Shift the first bubble only if there are more bubbles in addition to overflow - translationX = mBubbleBarPadding + ( - bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED - ? mIconOverlapAmount : 0); + // Shift the first bubble only if there are more bubbles + if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) { + translationX = mIconOverlapAmount; + } else { + translationX = 0f; + } } else { - translationX = mBubbleBarPadding + (bubbleIndex == 0 ? 0 : mIconOverlapAmount); + // when the bar is on the right, the first bubble always has translation 0. the only + // case where another bubble has translation 0 is when we only have 1 bubble and the + // overflow. otherwise all other bubbles should be shifted by the overlap amount. + if (bubbleIndex == 0 || getBubbleChildCount() == 1) { + translationX = 0f; + } else { + translationX = mIconOverlapAmount; + } } - return translationX - getScaleIconShift(); + return mBubbleBarPadding + translationX - getScaleIconShift(); + } + + private float getBubbleTranslationY() { + float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); + float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); + // When translating X & Y the scale is ignored, so need to deduct it from the translations + return mBubbleOffsetY + bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); } /** @@ -960,6 +1180,7 @@ public class BubbleBarView extends FrameLayout { } updateBubblesLayoutProperties(mBubbleBarLocation); updateContentDescription(); + updateNotificationDotsIfCollapsed(); } } @@ -979,9 +1200,19 @@ public class BubbleBarView extends FrameLayout { BubbleView previouslySelectedBubble = mSelectedBubbleView; mSelectedBubbleView = view; mBubbleBarBackground.showArrow(view != null); - // TODO: (b/283309949) remove animation should be implemented first, so than arrow - // animation is adjusted, skip animation for now - updateArrowForSelected(previouslySelectedBubble != null); + + // if bubbles are being animated, the arrow position will be set as part of the animation + if (mBubbleAnimator == null) { + updateArrowForSelected(previouslySelectedBubble != null); + } + if (view != null) { + if (isExpanded()) { + view.markSeen(); + } else { + // when collapsed, the selected bubble should show the dot if it has it + view.showDotIfNeeded(/* animate= */ true); + } + } } /** @@ -994,6 +1225,8 @@ public class BubbleBarView extends FrameLayout { mDraggedBubbleView = view; if (view != null) { view.setZ(mDragElevation); + // we started dragging a bubble. reset the bubble that was previously dismissed by drag + mDismissedByDragBubbleView = null; } setIsDragging(view != null); } @@ -1036,6 +1269,9 @@ public class BubbleBarView extends FrameLayout { } private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) { + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding; + } final int index = indexOfChild(mSelectedBubbleView); final float selectedBubbleTranslationX = getExpandedBubbleTranslationX( index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl())); @@ -1084,6 +1320,7 @@ public class BubbleBarView extends FrameLayout { mWidthAnimator.reverse(); } updateBubbleAccessibilityStates(); + announceExpandedStateChange(); } } @@ -1094,6 +1331,13 @@ public class BubbleBarView extends FrameLayout { return mIsBarExpanded; } + /** + * Returns whether the bubble bar is expanding. + */ + public boolean isExpanding() { + return mWidthAnimator.isRunning() && mIsBarExpanded; + } + /** * Get width of the bubble bar as if it would be expanded. * @@ -1101,32 +1345,34 @@ public class BubbleBarView extends FrameLayout { */ public float expandedWidth() { final int childCount = getChildCount(); - // spaces amount is less than child count by 1, or 0 if no child views - final float totalSpace; - final float totalIconSize; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - // when this animation is running, a new bubble is animating in while the bubble bar is - // expanded, so we have at least 2 bubbles in the bubble bar. - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing; - totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize(); - } else { - totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; - totalIconSize = childCount * getScaledIconSize(); + final float horizontalPadding = 2 * mBubbleBarPadding; + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getExpandedWidth() + horizontalPadding; } - return totalIconSize + totalSpace + 2 * mBubbleBarPadding; + // spaces amount is less than child count by 1, or 0 if no child views + final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; + final float totalIconSize = childCount * getScaledIconSize(); + return totalIconSize + totalSpace + horizontalPadding; } - private float collapsedWidth() { - final int childCount = getChildCount(); + /** + * Get width of the bubble bar if it is collapsed + */ + float collapsedWidth() { + final int bubbleChildCount = getBubbleChildCount(); final float horizontalPadding = 2 * mBubbleBarPadding; - // If there are more than 2 bubbles, the first 2 should be visible when collapsed. - // Otherwise just the first bubble should be visible because we don't show the overflow. - return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED + // If there are more than 2 bubbles, the first 2 should be visible when collapsed, + // excluding the overflow. + return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding : getScaledIconSize() + horizontalPadding; } + /** Returns the child count excluding the overflow if it's present. */ + int getBubbleChildCount() { + return hasOverflow() ? getChildCount() - 1 : getChildCount(); + } + private float getBubbleBarExpandedHeight() { return getBubbleBarCollapsedHeight() + mPointerSize; } @@ -1141,7 +1387,7 @@ public class BubbleBarView extends FrameLayout { * touch bounds. */ public boolean isEventOverAnyItem(MotionEvent ev) { - if (getVisibility() == View.VISIBLE) { + if (getVisibility() == VISIBLE) { getBoundsOnScreen(mTempRect); return mTempRect.contains((int) ev.getX(), (int) ev.getY()); } @@ -1150,9 +1396,7 @@ public class BubbleBarView extends FrameLayout { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (mIsAnimatingNewBubble) { - mController.onBubbleBarTouchedWhileAnimating(); - } + mController.onBubbleBarTouched(); if (!mIsBarExpanded) { // When the bar is collapsed, all taps on it should expand it. return true; @@ -1160,14 +1404,8 @@ public class BubbleBarView extends FrameLayout { return super.onInterceptTouchEvent(ev); } - /** Whether a new bubble is currently animating. */ - public boolean isAnimatingNewBubble() { - return mIsAnimatingNewBubble; - } - - - private boolean hasOverview() { - // Overview is always the last bubble + private boolean hasOverflow() { + // Overflow is always the last bubble View lastChild = getChildAt(getChildCount() - 1); if (lastChild instanceof BubbleView bubbleView) { return bubbleView.getBubble() instanceof BubbleBarOverflow; @@ -1176,21 +1414,39 @@ public class BubbleBarView extends FrameLayout { } private void updateBubbleAccessibilityStates() { - final int childA11y; if (mIsBarExpanded) { // Bar is expanded, focus on the bubbles setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES; + + // Set up a11y navigation order. Get list of child views in the order they are shown + // on screen. And use that to set up navigation so that swiping left focuses the view + // on the left and swiping right focuses view on the right. + View prevChild = null; + for (View childView : getChildViewsInOnScreenOrder()) { + childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + childView.setFocusable(true); + final View finalPrevChild = prevChild; + // Always need to set a new delegate to clear out any previous. + childView.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (finalPrevChild != null) { + info.setTraversalAfter(finalPrevChild); + } + } + }); + prevChild = childView; + } } else { // Bar is collapsed, only focus on the bar setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO; - } - for (int i = 0; i < getChildCount(); i++) { - getChildAt(i).setImportantForAccessibility(childA11y); - // Only allowing focusing on bubbles when bar is expanded. Otherwise, in talkback mode, - // bubbles can be navigates to in collapsed mode. - getChildAt(i).setFocusable(mIsBarExpanded); + for (int i = 0; i < getChildCount(); i++) { + View childView = getChildAt(i); + childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + childView.setFocusable(false); + } } } @@ -1199,7 +1455,7 @@ public class BubbleBarView extends FrameLayout { CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : ""; // Don't count overflow if it exists - int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0); + int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0); if (bubbleCount > 1) { contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles, contentDesc, bubbleCount - 1); @@ -1207,6 +1463,26 @@ public class BubbleBarView extends FrameLayout { setContentDescription(contentDesc); } + private void announceExpandedStateChange() { + final CharSequence selectedBubbleContentDesc; + if (mSelectedBubbleView != null) { + selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription(); + } else { + selectedBubbleContentDesc = getResources().getString( + R.string.bubble_bar_bubble_fallback_description); + } + + final String msg; + if (mIsBarExpanded) { + msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand, + selectedBubbleContentDesc); + } else { + msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse, + selectedBubbleContentDesc); + } + announceForAccessibility(msg); + } + private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) { return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding); } @@ -1247,13 +1523,54 @@ public class BubbleBarView extends FrameLayout { }); } + /** Dumps the current state of BubbleBarView. */ + public void dump(PrintWriter pw) { + pw.println("BubbleBarView state:"); + pw.println(" visibility: " + getVisibility()); + pw.println(" alpha: " + getAlpha()); + pw.println(" translationY: " + getTranslationY()); + pw.println(" childCount: " + getChildCount()); + pw.println(" hasOverflow: " + hasOverflow()); + for (BubbleView bubbleView: getBubbles()) { + BubbleBarItem bubble = bubbleView.getBubble(); + String key = bubble == null ? "null" : bubble.getKey(); + pw.println(" bubble key: " + key); + } + pw.println(" isExpanded: " + isExpanded()); + if (mBubbleAnimator != null) { + pw.println(" mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning()); + pw.println(" mBubbleAnimator is null"); + } + pw.println(" mDragging: " + mDragging); + } + + private List getBubbles() { + List bubbles = new ArrayList<>(); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof BubbleView bubble) { + bubbles.add(bubble); + } + } + return bubbles; + } + /** Interface for BubbleBarView to communicate with its controller. */ interface Controller { /** Returns the translation Y that the bubble bar should have. */ float getBubbleBarTranslationY(); - /** Notifies the controller that the bubble bar was touched while it was animating. */ - void onBubbleBarTouchedWhileAnimating(); + /** Notifies the controller that the bubble bar was touched. */ + void onBubbleBarTouched(); + + /** Requests the controller to expand bubble bar */ + void expandBubbleBar(); + + /** Requests the controller to dismiss the bubble bar */ + void dismissBubbleBar(); + + /** Requests the controller to update bubble bar location to the given value */ + void updateBubbleBarLocation(BubbleBarLocation location); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index eec095df56..025c03860c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -18,6 +18,8 @@ package com.android.launcher3.taskbar.bubbles; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; +import android.animation.Animator; +import android.animation.AnimatorSet; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; @@ -25,26 +27,28 @@ import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Gravity; import android.view.MotionEvent; import android.view.View; -import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.SystemUiProxy; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import java.io.PrintWriter; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -63,6 +67,7 @@ public class BubbleBarViewController { private final TaskbarActivityContext mActivity; private final BubbleBarView mBarView; private int mIconSize; + private int mBubbleBarPadding; // Initialized in init. private BubbleStashController mBubbleStashController; @@ -70,17 +75,32 @@ public class BubbleBarViewController { private BubbleDragController mBubbleDragController; private TaskbarStashController mTaskbarStashController; private TaskbarInsetsController mTaskbarInsetsController; + private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider; private View.OnClickListener mBubbleClickListener; private View.OnClickListener mBubbleBarClickListener; + private BubbleView.Controller mBubbleViewController; + private BubbleBarOverflow mOverflowBubble; // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing private final MultiValueAlpha mBubbleBarAlpha; - private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale); + private final AnimatedFloat mBubbleBarBubbleAlpha = new AnimatedFloat(this::updateBubbleAlpha); + private final AnimatedFloat mBubbleBarBackgroundAlpha = new AnimatedFloat( + this::updateBackgroundAlpha); + private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX); + private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY); + private final AnimatedFloat mBubbleBarBackgroundScaleX = new AnimatedFloat( + this::updateBackgroundScaleX); + private final AnimatedFloat mBubbleBarBackgroundScaleY = new AnimatedFloat( + this::updateBackgroundScaleY); private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat( this::updateTranslationY); + private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat( + this::updateBubbleOffsetY); // Modified when swipe up is happening on the bubble bar or task bar. private float mBubbleBarSwipeUpTranslationY; + // Modified when bubble bar is springing back into the stash handle. + private float mBubbleBarStashTranslationY; // Whether the bar is hidden for a sysui state. private boolean mHiddenForSysui; @@ -88,8 +108,12 @@ public class BubbleBarViewController { private boolean mHiddenForNoBubbles = true; private boolean mShouldShowEducation; + public boolean mOverflowAdded; + private BubbleBarViewAnimator mBubbleBarViewAnimator; + private final TimeSource mTimeSource = System::currentTimeMillis; + @Nullable private BubbleBarBoundsChangeListener mBoundsChangeListener; @@ -102,21 +126,25 @@ public class BubbleBarViewController { R.dimen.bubblebar_icon_size); } - public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { + /** Initializes controller. */ + public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers, + TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) { mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarController = bubbleControllers.bubbleBarController; mBubbleDragController = bubbleControllers.bubbleDragController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; - mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController); - + mBubbleBarViewAnimator = new BubbleBarViewAnimator( + mBarView, mBubbleStashController, mBubbleBarController::showExpandedView); + mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider; + onBubbleBarConfigurationChanged(/* animate= */ false); mActivity.addOnDeviceProfileChangeListener( - dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true)); - updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false); - mBubbleBarScale.updateValue(1f); - mBubbleClickListener = v -> onBubbleClicked(v); - mBubbleBarClickListener = v -> onBubbleBarClicked(); + dp -> onBubbleBarConfigurationChanged(/* animate= */ true)); + mBubbleBarScaleY.updateValue(1f); + mBubbleClickListener = v -> onBubbleClicked((BubbleView) v); + mBubbleBarClickListener = v -> expandBubbleBar(); mBubbleDragController.setupBubbleBarView(mBarView); + mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView); mBarView.setOnClickListener(mBubbleBarClickListener); mBarView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { @@ -132,14 +160,55 @@ public class BubbleBarViewController { } @Override - public void onBubbleBarTouchedWhileAnimating() { - BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating(); + public void onBubbleBarTouched() { + BubbleBarViewController.this.onBubbleBarTouched(); + } + + @Override + public void expandBubbleBar() { + BubbleBarViewController.this.expandBubbleBar(); + } + + @Override + public void dismissBubbleBar() { + onDismissAllBubbles(); + } + + @Override + public void updateBubbleBarLocation(BubbleBarLocation location) { + mBubbleBarController.updateBubbleBarLocation(location); } }); + + mBubbleViewController = new BubbleView.Controller() { + @Override + public BubbleBarLocation getBubbleBarLocation() { + return BubbleBarViewController.this.getBubbleBarLocation(); + } + + @Override + public void dismiss(BubbleView bubble) { + if (bubble.getBubble() != null) { + notifySysUiBubbleDismissed(bubble.getBubble()); + } + onBubbleDismissed(bubble); + } + + @Override + public void collapse() { + collapseBubbleBar(); + } + + @Override + public void updateBubbleBarLocation(BubbleBarLocation location) { + mBubbleBarController.updateBubbleBarLocation(location); + } + }; } - private void onBubbleClicked(View v) { - BubbleBarItem bubble = ((BubbleView) v).getBubble(); + private void onBubbleClicked(BubbleView bubbleView) { + bubbleView.markSeen(); + BubbleBarItem bubble = bubbleView.getBubble(); if (bubble == null) { Log.e(TAG, "bubble click listener, bubble was null"); } @@ -147,19 +216,21 @@ public class BubbleBarViewController { final String currentlySelected = mBubbleBarController.getSelectedBubbleKey(); if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) { // Tapping the currently selected bubble while expanded collapses the view. - setExpanded(false); - mBubbleStashController.stashBubbleBar(); + collapseBubbleBar(); } else { mBubbleBarController.showAndSelectBubble(bubble); } } - private void onBubbleBarTouchedWhileAnimating() { - mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating(); - mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY()); + private void onBubbleBarTouched() { + if (isAnimatingNewBubble()) { + mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating(); + mBubbleStashController.onNewBubbleAnimationInterrupted(false, + mBarView.getTranslationY()); + } } - private void onBubbleBarClicked() { + private void expandBubbleBar() { if (mShouldShowEducation) { mShouldShowEducation = false; // Get the bubble bar bounds on screen @@ -178,6 +249,11 @@ public class BubbleBarViewController { } } + private void collapseBubbleBar() { + setExpanded(false); + mBubbleStashController.stashBubbleBar(); + } + /** Notifies that the stash state is changing. */ public void onStashStateChanging() { if (isAnimatingNewBubble()) { @@ -194,18 +270,67 @@ public class BubbleBarViewController { return mBubbleBarAlpha; } - public AnimatedFloat getBubbleBarScale() { - return mBubbleBarScale; + public AnimatedFloat getBubbleBarBubbleAlpha() { + return mBubbleBarBubbleAlpha; + } + + public AnimatedFloat getBubbleBarBackgroundAlpha() { + return mBubbleBarBackgroundAlpha; + } + + public AnimatedFloat getBubbleBarScaleX() { + return mBubbleBarScaleX; + } + + public AnimatedFloat getBubbleBarScaleY() { + return mBubbleBarScaleY; + } + + public AnimatedFloat getBubbleBarBackgroundScaleX() { + return mBubbleBarBackgroundScaleX; + } + + public AnimatedFloat getBubbleBarBackgroundScaleY() { + return mBubbleBarBackgroundScaleY; } public AnimatedFloat getBubbleBarTranslationY() { return mBubbleBarTranslationY; } - float getBubbleBarCollapsedHeight() { + public AnimatedFloat getBubbleOffsetY() { + return mBubbleOffsetY; + } + + public float getBubbleBarCollapsedWidth() { + return mBarView.collapsedWidth(); + } + + public float getBubbleBarCollapsedHeight() { return mBarView.getBubbleBarCollapsedHeight(); } + /** + * @see BubbleBarView#getRelativePivotX() + */ + public float getBubbleBarRelativePivotX() { + return mBarView.getRelativePivotX(); + } + + /** + * @see BubbleBarView#getRelativePivotY() + */ + public float getBubbleBarRelativePivotY() { + return mBarView.getRelativePivotY(); + } + + /** + * @see BubbleBarView#setRelativePivot(float, float) + */ + public void setBubbleBarRelativePivot(float x, float y) { + mBarView.setRelativePivot(x, y); + } + /** * Whether the bubble bar is visible or not. */ @@ -225,6 +350,14 @@ public class BubbleBarViewController { return mBarView.getBubbleBarLocation(); } + /** + * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on + * the right + */ + public boolean isBubbleBarOnLeft() { + return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); + } + /** * Update bar {@link BubbleBarLocation} */ @@ -248,9 +381,22 @@ public class BubbleBarViewController { return mBarView.getBubbleBarBounds(); } + /** Checks that bubble bar is visible and that the motion event is within bounds. */ + public boolean isEventOverBubbleBar(MotionEvent event) { + if (!isBubbleBarVisible()) return false; + final Rect bounds = getBubbleBarBounds(); + final int bubbleBarTopOnScreen = mBarView.getRestingTopPositionOnScreen(); + final float x = event.getX(); + return event.getRawY() >= bubbleBarTopOnScreen && x >= bounds.left && x <= bounds.right; + } + /** Whether a new bubble is animating. */ public boolean isAnimatingNewBubble() { - return mBarView.isAnimatingNewBubble(); + return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating(); + } + + public boolean isNewBubbleAnimationRunningOrPending() { + return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation(); } /** The horizontal margin of the bubble bar from the edge of the screen. */ @@ -294,6 +440,7 @@ public class BubbleBarViewController { if (hidden) { mBarView.setAlpha(0); mBarView.setExpanded(false); + adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded = */ false); } mActivity.bubbleBarVisibilityChanged(!hidden); } @@ -333,27 +480,61 @@ public class BubbleBarViewController { // Modifying view related properties. // - private void updateBubbleBarIconSize(int newIconSize, boolean animate) { + /** Notifies controller of configuration change, so bubble bar can be adjusted */ + public void onBubbleBarConfigurationChanged(boolean animate) { + int newIconSize; + int newPadding; Resources res = mActivity.getResources(); + if (mBubbleStashController.isBubblesShowingOnHome() + || mBubbleStashController.isTransientTaskBar()) { + newIconSize = getBubbleBarIconSizeFromDeviceProfile(res); + newPadding = getBubbleBarPaddingFromDeviceProfile(res); + } else { + // the bubble bar is shown inside the persistent task bar, use preset sizes + newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar); + newPadding = res.getDimensionPixelSize( + R.dimen.bubblebar_icon_spacing_persistent_taskbar); + } + updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate); + } + + + private int getBubbleBarIconSizeFromDeviceProfile(Resources res) { + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); DisplayMetrics dm = res.getDisplayMetrics(); float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_SMALL_DP, dm); float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_MEDIUM_DP, dm); - float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - APP_ICON_LARGE_DP, dm); float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f; - float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f; - mIconSize = newIconSize <= smallMediumThreshold + int taskbarIconSize = deviceProfile.taskbarIconSize; + return taskbarIconSize <= smallMediumThreshold ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) : res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); - float bubbleBarPadding = newIconSize >= mediumLargeThreshold + + } + + private int getBubbleBarPaddingFromDeviceProfile(Resources res) { + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); + DisplayMetrics dm = res.getDisplayMetrics(); + float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + APP_ICON_MEDIUM_DP, dm); + float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + APP_ICON_LARGE_DP, dm); + float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f; + return deviceProfile.taskbarIconSize >= mediumLargeThreshold ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) : res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); + } + + private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) { + if (mIconSize == iconSize && mBubbleBarPadding == padding) return; + mIconSize = iconSize; + mBubbleBarPadding = padding; if (animate) { - mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding); + mBarView.animateBubbleBarIconSize(iconSize, padding); } else { - mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding); + mBarView.setIconSizeAndPadding(iconSize, padding); } } @@ -365,20 +546,47 @@ public class BubbleBarViewController { updateTranslationY(); } - private void updateTranslationY() { - mBarView.setTranslationY(mBubbleBarTranslationY.value - + mBubbleBarSwipeUpTranslationY); + /** + * Sets the translation of the bubble bar during the stash animation. + */ + public void setTranslationYForStash(float transY) { + mBubbleBarStashTranslationY = transY; + updateTranslationY(); } - /** - * Applies scale properties for the entire bubble bar. - */ - private void updateScale() { - float scale = mBubbleBarScale.value; + private void updateTranslationY() { + mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY + + mBubbleBarStashTranslationY); + } + + private void updateScaleX(float scale) { mBarView.setScaleX(scale); + } + + private void updateScaleY(float scale) { mBarView.setScaleY(scale); } + private void updateBackgroundScaleX(float scale) { + mBarView.setBackgroundScaleX(scale); + } + + private void updateBackgroundScaleY(float scale) { + mBarView.setBackgroundScaleY(scale); + } + + private void updateBubbleAlpha(float alpha) { + mBarView.setBubbleAlpha(alpha); + } + + private void updateBubbleOffsetY(float transY) { + mBarView.setBubbleOffsetY(transY); + } + + private void updateBackgroundAlpha(float alpha) { + mBarView.setBackgroundAlpha(alpha); + } + // // Manipulating the specific bubble views in the bar // @@ -386,58 +594,119 @@ public class BubbleBarViewController { /** * Removes the provided bubble from the bubble bar. */ - public void removeBubble(BubbleBarItem b) { + public void removeBubble(BubbleBarBubble b) { if (b != null) { - mBarView.removeView(b.getView()); + mBarView.removeBubble(b.getView()); + b.getView().setController(null); } else { Log.w(TAG, "removeBubble, bubble was null!"); } } + /** Adds a new bubble and removes an old bubble at the same time. */ + public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, + BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation, + boolean addOverflowToo) { + mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(), + addOverflowToo ? () -> showOverflow(true) : null); + addedBubble.getView().setOnClickListener(mBubbleClickListener); + addedBubble.getView().setController(mBubbleViewController); + removedBubble.getView().setController(null); + mBubbleDragController.setupBubbleView(addedBubble.getView()); + if (!suppressAnimation) { + animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false); + } + } + + /** Whether the overflow view is added to the bubble bar. */ + public boolean isOverflowAdded() { + return mOverflowAdded; + } + + /** Shows or hides the overflow view. */ + public void showOverflow(boolean showOverflow) { + if (mOverflowAdded == showOverflow) return; + mOverflowAdded = showOverflow; + if (mOverflowAdded) { + mBarView.addBubble(mOverflowBubble.getView()); + mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); + mOverflowBubble.getView().setController(mBubbleViewController); + } else { + mBarView.removeBubble(mOverflowBubble.getView()); + mOverflowBubble.getView().setOnClickListener(null); + mOverflowBubble.getView().setController(null); + } + } + + /** Adds the overflow view to the bubble bar while animating a view away. */ + public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) { + if (mOverflowAdded) return; + mOverflowAdded = true; + mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(), + null /* onEndRunnable */); + mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); + mOverflowBubble.getView().setController(mBubbleViewController); + removedBubble.getView().setController(null); + } + + /** Removes the overflow view to the bubble bar while animating a view in. */ + public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) { + if (!mOverflowAdded) return; + mOverflowAdded = false; + mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(), + null /* onEndRunnable */); + addedBubble.getView().setOnClickListener(mBubbleClickListener); + addedBubble.getView().setController(mBubbleViewController); + mOverflowBubble.getView().setController(null); + } + /** * Adds the provided bubble to the bubble bar. */ public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) { if (b != null) { - mBarView.addBubble( - b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT)); + mBarView.addBubble(b.getView()); b.getView().setOnClickListener(mBubbleClickListener); mBubbleDragController.setupBubbleView(b.getView()); - - if (b instanceof BubbleBarOverflow) { - return; - } + b.getView().setController(mBubbleViewController); if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) { // the bubble bar and handle are initialized as part of the first bubble animation. // if the animation is suppressed, immediately stash or show the bubble bar to // ensure they've been initialized. - if (mTaskbarStashController.isInApp()) { + if (mTaskbarStashController.isInApp() + && mBubbleStashController.isTransientTaskBar()) { mBubbleStashController.stashBubbleBarImmediate(); } else { mBubbleStashController.showBubbleBarImmediate(); } return; } - animateBubbleNotification(bubble, isExpanding); + animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false); } else { Log.w(TAG, "addBubble, bubble was null!"); } } /** Animates the bubble bar to notify the user about a bubble change. */ - public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) { + public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding, + boolean isUpdate) { boolean isInApp = mTaskbarStashController.isInApp(); - // if this is the first bubble, animate to the initial state. one bubble is the overflow - // so check for at most 2 children. - if (mBarView.getChildCount() <= 2) { + // if this is the first bubble, animate to the initial state. + if (mBarView.getBubbleChildCount() == 1 && !isUpdate) { mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding); return; } + boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome() + || !mBubbleStashController.isTransientTaskBar(); + if (persistentTaskbarOrOnHome && !isExpanded()) { + mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding); + return; + } - // only animate the new bubble if we're in an app and not auto expanding - if (isInApp && !isExpanding && !isExpanded()) { - mBubbleBarViewAnimator.animateBubbleInForStashed(bubble); + // only animate the new bubble if we're in an app, have handle view and not auto expanding + if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) { + mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding); } } @@ -466,6 +735,7 @@ public class BubbleBarViewController { public void setExpanded(boolean isExpanded) { if (isExpanded != mBarView.isExpanded()) { mBarView.setExpanded(isExpanded); + adjustTaskbarAndHotseatToBubbleBarState(isExpanded); if (!isExpanded) { mSystemUiProxy.collapseBubbles(); } else { @@ -476,11 +746,46 @@ public class BubbleBarViewController { } } + /** + * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in + * app or overview. Set the hotseat stashed state if on launcher home screen. If not on launcher + * home screen and hotseat is stashed immediately un-stashes the hotseat. + */ + private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) { + if (mBubbleStashController.isBubblesShowingOnHome()) { + mTaskbarStashController.stashHotseat(isBubbleBarExpanded); + } else if (!mBubbleStashController.isTransientTaskBar()) { + boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar(); + mTaskbarViewPropertiesProvider + .getIconsAlpha() + .animateToValue(hideTaskbar ? 0 : 1) + .start(); + } + if (!mBubbleStashController.isBubblesShowingOnHome() + && mTaskbarStashController.isHiddenForBubbles()) { + mTaskbarStashController.unStashHotseatInstantly(); + } + } + + /** Return {@code true} if expanded bubble bar would intersect the taskbar. */ + public boolean isIntersectingTaskbar() { + if (mBarView.isExpanding() || mBarView.isExpanded()) { + Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds(); + return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds); + } else { + return false; + } + } + /** * Sets whether the bubble bar should be expanded. This method is used in response to UI events * from SystemUI. */ public void setExpandedFromSysui(boolean isExpanded) { + if (isNewBubbleAnimationRunningOrPending() && isExpanded) { + mBubbleBarViewAnimator.expandedWhileAnimating(); + return; + } if (!isExpanded) { mBubbleStashController.stashBubbleBar(); } else { @@ -497,6 +802,7 @@ public class BubbleBarViewController { /** * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI * that a bubble is being dragged to dismiss. + * * @param bubbleView dragged bubble view */ public void onBubbleDragStart(@NonNull BubbleView bubbleView) { @@ -513,6 +819,12 @@ public class BubbleBarViewController { mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen()); } + /** Handle given bubble being dismissed */ + public void onBubbleDismissed(BubbleView bubble) { + mBubbleBarController.onBubbleDismissed(bubble); + mBarView.removeBubble(bubble); + } + /** * Notifies {@link BubbleBarView} that drag and all animations are finished. */ @@ -551,17 +863,16 @@ public class BubbleBarViewController { } /** - * Called when bubble was dragged into the dismiss target. Notifies System - * @param bubble dismissed bubble item + * Notify SystemUI that the given bubble has been dismissed. */ - public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) { - mSystemUiProxy.dragBubbleToDismiss(bubble.getKey()); + public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) { + mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis()); } /** - * Called when bubble stack was dragged into the dismiss target + * Called when bubble stack was dismissed */ - public void onDismissAllBubblesWhileDragging() { + public void onDismissAllBubbles() { mSystemUiProxy.removeAllBubbles(); } @@ -572,6 +883,53 @@ public class BubbleBarViewController { mBoundsChangeListener = listener; } + /** Called when the controller is destroyed. */ + public void onDestroy() { + adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false); + } + + /** + * Create an animator for showing or hiding bubbles when stashed state changes + * + * @param isStashed {@code true} when bubble bar should be stashed to the handle + */ + public Animator createRevealAnimatorForStashChange(boolean isStashed) { + Rect stashedHandleBounds = new Rect(); + mBubbleStashController.getHandleBounds(stashedHandleBounds); + int childCount = mBarView.getChildCount(); + float newChildWidth = (float) stashedHandleBounds.width() / childCount; + AnimatorSet animatorSet = new AnimatorSet(); + for (int i = 0; i < childCount; i++) { + BubbleView child = (BubbleView) mBarView.getChildAt(i); + animatorSet.play( + createRevealAnimForBubble(child, isStashed, stashedHandleBounds, + newChildWidth)); + } + return animatorSet; + } + + private Animator createRevealAnimForBubble(BubbleView bubbleView, boolean isStashed, + Rect stashedHandleBounds, float newWidth) { + Rect viewBounds = new Rect(0, 0, bubbleView.getWidth(), bubbleView.getHeight()); + + int viewCenterY = viewBounds.centerY(); + int halfHandleHeight = stashedHandleBounds.height() / 2; + int widthDelta = Math.max(0, (int) (viewBounds.width() - newWidth) / 2); + + Rect stashedViewBounds = new Rect( + viewBounds.left + widthDelta, + viewCenterY - halfHandleHeight, + viewBounds.right - widthDelta, + viewCenterY + halfHandleHeight + ); + + float viewRadius = 0f; // Use 0 to not clip the new message dot or the app icon + float stashedRadius = stashedViewBounds.height() / 2f; + + return new RoundedRectRevealOutlineProvider(viewRadius, stashedRadius, viewBounds, + stashedViewBounds).createRevealAnimator(bubbleView, !isStashed, 0); + } + /** * Listener to receive updates about bubble bar bounds changing */ @@ -579,4 +937,35 @@ public class BubbleBarViewController { /** Called when bounds have changed */ void onBoundsChanged(); } + + /** Interface for getting the current timestamp. */ + interface TimeSource { + long currentTimeMillis(); + } + + /** Dumps the state of BubbleBarViewController. */ + public void dump(PrintWriter pw) { + pw.println("Bubble bar view controller state:"); + pw.println(" mHiddenForSysui: " + mHiddenForSysui); + pw.println(" mHiddenForNoBubbles: " + mHiddenForNoBubbles); + pw.println(" mShouldShowEducation: " + mShouldShowEducation); + pw.println(" mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value); + pw.println(" mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY); + pw.println(" mOverflowAdded: " + mOverflowAdded); + if (mBarView != null) { + mBarView.dump(pw); + } else { + pw.println(" Bubble bar view is null!"); + } + } + + /** Interface for BubbleBarViewController to get the taskbar view properties. */ + public interface TaskbarViewPropertiesProvider { + + /** Returns the bounds of the taskbar. */ + Rect getTaskbarViewBounds(); + + /** Returns taskbar icons alpha */ + MultiPropertyFactory.MultiProperty getIconsAlpha(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index 32d6375462..a66df4c357 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -15,22 +15,32 @@ */ package com.android.launcher3.taskbar.bubbles; +import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR; + +import android.graphics.Rect; +import android.view.View; + import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.RunnableList; -/** - * Hosts various bubble controllers to facilitate passing between one another. - */ +import java.io.PrintWriter; +import java.util.Optional; + +/** Hosts various bubble controllers to facilitate passing between one another. */ public class BubbleControllers { public final BubbleBarController bubbleBarController; public final BubbleBarViewController bubbleBarViewController; public final BubbleStashController bubbleStashController; - public final BubbleStashedHandleViewController bubbleStashedHandleViewController; + public final Optional bubbleStashedHandleViewController; public final BubbleDragController bubbleDragController; public final BubbleDismissController bubbleDismissController; public final BubbleBarPinController bubbleBarPinController; public final BubblePinController bubblePinController; + public final BubbleCreator bubbleCreator; private final RunnableList mPostInitRunnables = new RunnableList(); @@ -43,11 +53,12 @@ public class BubbleControllers { BubbleBarController bubbleBarController, BubbleBarViewController bubbleBarViewController, BubbleStashController bubbleStashController, - BubbleStashedHandleViewController bubbleStashedHandleViewController, + Optional bubbleStashedHandleViewController, BubbleDragController bubbleDragController, BubbleDismissController bubbleDismissController, BubbleBarPinController bubbleBarPinController, - BubblePinController bubblePinController) { + BubblePinController bubblePinController, + BubbleCreator bubbleCreator) { this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; this.bubbleStashController = bubbleStashController; @@ -56,6 +67,7 @@ public class BubbleControllers { this.bubbleDismissController = bubbleDismissController; this.bubbleBarPinController = bubbleBarPinController; this.bubblePinController = bubblePinController; + this.bubbleCreator = bubbleCreator; } /** @@ -64,14 +76,40 @@ public class BubbleControllers { * in constructors for now, as some controllers may still be waiting for init(). */ public void init(TaskbarControllers taskbarControllers) { + // TODO(b/346381754) add TaskbarLauncherStateController implementation to adjust the hotseat + BubbleBarLocationCompositeListener bubbleBarLocationListeners = + new BubbleBarLocationCompositeListener( + taskbarControllers.navbarButtonsViewController, + taskbarControllers.taskbarViewController + ); bubbleBarController.init(this, + bubbleBarLocationListeners, taskbarControllers.navbarButtonsViewController::isImeVisible); - bubbleBarViewController.init(taskbarControllers, this); - bubbleStashedHandleViewController.init(taskbarControllers, this); - bubbleStashController.init(taskbarControllers, this); + bubbleStashedHandleViewController.ifPresent( + controller -> controller.init(/* bubbleControllers = */ this)); + bubbleStashController.init( + taskbarControllers.taskbarInsetsController, + bubbleBarViewController, + bubbleStashedHandleViewController.orElse(null), + taskbarControllers::runAfterInit + ); + bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this, + new TaskbarViewPropertiesProvider() { + @Override + public Rect getTaskbarViewBounds() { + return taskbarControllers.taskbarViewController.getIconLayoutBounds(); + } + + @Override + public MultiPropertyFactory.MultiProperty getIconsAlpha() { + return taskbarControllers.taskbarViewController + .getTaskbarIconAlpha() + .get(ALPHA_INDEX_BUBBLE_BAR); + } + }); bubbleDragController.init(/* bubbleControllers = */ this); bubbleDismissController.init(/* bubbleControllers = */ this); - bubbleBarPinController.init(this); + bubbleBarPinController.init(this, bubbleBarLocationListeners); bubblePinController.init(this); mPostInitRunnables.executeAllAndDestroy(); @@ -91,7 +129,13 @@ public class BubbleControllers { * Cleans up all controllers. */ public void onDestroy() { - bubbleStashedHandleViewController.onDestroy(); + bubbleStashedHandleViewController.ifPresent(BubbleStashedHandleViewController::onDestroy); bubbleBarController.onDestroy(); + bubbleBarViewController.onDestroy(); + } + + /** Dumps bubble controllers state. */ + public void dump(PrintWriter pw) { + bubbleBarViewController.dump(pw); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java new file mode 100644 index 0000000000..12b1487552 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java @@ -0,0 +1,221 @@ +/* + * 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 static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; + +import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.UserHandle; +import android.util.Log; +import android.util.PathParser; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.appcompat.content.res.AppCompatResources; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.R; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.launcher3.shortcuts.ShortcutRequest; +import com.android.wm.shell.shared.bubbles.BubbleInfo; + +/** + * Loads the necessary info to populate / present a bubble (name, icon, shortcut). + */ +public class BubbleCreator { + + private static final String TAG = BubbleCreator.class.getSimpleName(); + + private final Context mContext; + private final LauncherApps mLauncherApps; + private final BubbleIconFactory mIconFactory; + + public BubbleCreator(Context context) { + mContext = context; + mLauncherApps = mContext.getSystemService(LauncherApps.class); + mIconFactory = new BubbleIconFactory(context, + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size), + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size), + context.getResources().getColor(R.color.important_conversation), + context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width)); + } + + /** + * Creates a BubbleBarBubble object, including the view if needed, and populates it with + * the info needed for presentation. + * + * @param context the context to use for inflation. + * @param info the info to use to populate the bubble. + * @param barView the parent view for the bubble (bubble is not added to the view). + * @param existingBubble if a bubble exists already, this object gets updated with the new + * info & returned (& any existing views are reused instead of inflating + * new ones. + */ + @Nullable + public BubbleBarBubble populateBubble(Context context, BubbleInfo info, ViewGroup barView, + @Nullable BubbleBarBubble existingBubble) { + String appName; + Bitmap badgeBitmap; + Bitmap bubbleBitmap; + Path dotPath; + int dotColor; + + boolean isImportantConvo = info.isImportantConversation(); + + ShortcutRequest.QueryResult result = new ShortcutRequest(context, + new UserHandle(info.getUserId())) + .forPackage(info.getPackageName(), info.getShortcutId()) + .query(FLAG_MATCH_DYNAMIC + | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER + | FLAG_MATCH_CACHED + | FLAG_GET_PERSONS_DATA); + + ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null; + if (shortcutInfo == null) { + Log.w(TAG, "No shortcutInfo found for bubble: " + info.getKey() + + " with shortcutId: " + info.getShortcutId()); + } + + ApplicationInfo appInfo; + try { + appInfo = mLauncherApps.getApplicationInfo( + info.getPackageName(), + 0, + new UserHandle(info.getUserId())); + } catch (PackageManager.NameNotFoundException e) { + // If we can't find package... don't think we should show the bubble. + Log.w(TAG, "Unable to find packageName: " + info.getPackageName()); + return null; + } + if (appInfo == null) { + Log.w(TAG, "Unable to find appInfo: " + info.getPackageName()); + return null; + } + PackageManager pm = context.getPackageManager(); + appName = String.valueOf(appInfo.loadLabel(pm)); + Drawable appIcon = appInfo.loadUnbadgedIcon(pm); + Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(info.getUserId())); + + // Badged bubble image + Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo, + info.getIcon()); + if (bubbleDrawable == null) { + // Default to app icon + bubbleDrawable = appIcon; + } + + BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo); + badgeBitmap = badgeBitmapInfo.icon; + + float[] bubbleBitmapScale = new float[1]; + bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); + + // Dot color & placement + Path iconPath = PathParser.createPathFromPathData( + context.getResources().getString( + com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = bubbleBitmapScale[0]; + float radius = BubbleView.DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + dotPath = iconPath; + dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, + Color.WHITE, WHITE_SCRIM_ALPHA / 255f); + + if (existingBubble == null) { + LayoutInflater inflater = LayoutInflater.from(context); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubblebar_item_view, barView, false /* attachToRoot */); + + BubbleBarBubble bubble = new BubbleBarBubble(info, bubbleView, + badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); + bubbleView.setBubble(bubble); + return bubble; + } else { + // If we already have a bubble (so it already has an inflated view), update it. + existingBubble.setInfo(info); + existingBubble.setBadge(badgeBitmap); + existingBubble.setIcon(bubbleBitmap); + existingBubble.setDotColor(dotColor); + existingBubble.setDotPath(dotPath); + existingBubble.setAppName(appName); + return existingBubble; + } + } + + /** + * Creates the overflow view shown in the bubble bar. + * + * @param barView the parent view for the bubble (bubble is not added to the view). + */ + public BubbleBarOverflow createOverflow(ViewGroup barView) { + Bitmap bitmap = createOverflowBitmap(); + LayoutInflater inflater = LayoutInflater.from(mContext); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubble_bar_overflow_button, barView, false /* attachToRoot */); + BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView); + bubbleView.setOverflow(overflow, bitmap); + return overflow; + } + + private Bitmap createOverflowBitmap() { + Drawable iconDrawable = AppCompatResources.getDrawable(mContext, + R.drawable.bubble_ic_overflow_button); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[]{ + R.attr.materialColorOnPrimaryFixed, + R.attr.materialColorPrimaryFixed + }); + int overflowIconColor = ta.getColor(0, Color.WHITE); + int overflowBackgroundColor = ta.getColor(1, Color.BLACK); + ta.recycle(); + + iconDrawable.setTint(overflowIconColor); + + int inset = mContext.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset); + Drawable foreground = new InsetDrawable(iconDrawable, inset); + Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor), + foreground); + + return mIconFactory.createBadgedIconBitmap(drawable).icon; + } + +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java index a6096e229c..a459dd9e43 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java @@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarDragLayer; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; /** * Controls dismiss view presentation for the bubble bar dismiss functionality. @@ -143,10 +143,10 @@ public class BubbleDismissController { if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) { BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject(); if (bubbleView.getBubble() != null) { - mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble()); + mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble()); } } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) { - mBubbleBarViewController.onDismissAllBubblesWhileDragging(); + mBubbleBarViewController.onDismissAllBubbles(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt index 6c3f0d8796..a8002a5088 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt @@ -18,7 +18,7 @@ package com.android.launcher3.taskbar.bubbles import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.DismissView /** * Dismiss view is shared from WMShell. It requires setup with local resources. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java index 7aed2d2abe..adaba7a242 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java @@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import com.android.launcher3.R; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.animation.PhysicsAnimator; /** @@ -128,7 +128,7 @@ public class BubbleDragAnimator { boolean wasFling, boolean canceled, float finalValue, float finalVelocity, boolean allRelevantPropertyAnimationsEnded) -> { if (canceled || allRelevantPropertyAnimationsEnded) { - resetAnimatedViews(restingPosition); + resetAnimatedViews(restingPosition, /* dismissed= */ false); if (endActions != null) { endActions.run(); } @@ -197,7 +197,7 @@ public class BubbleDragAnimator { boolean wasFling, boolean canceled, float finalValue, float finalVelocity, boolean allRelevantPropertyAnimationsEnded) -> { if (canceled || allRelevantPropertyAnimationsEnded) { - resetAnimatedViews(initialPosition); + resetAnimatedViews(initialPosition, /* dismissed= */ true); if (endActions != null) endActions.run(); } }) @@ -208,11 +208,14 @@ public class BubbleDragAnimator { * Reset the animated views to the initial state * * @param initialPosition position of the bubble + * @param dismissed whether the animated view was dismissed */ - private void resetAnimatedViews(@NonNull PointF initialPosition) { + private void resetAnimatedViews(@NonNull PointF initialPosition, boolean dismissed) { mView.setScaleX(1f); mView.setScaleY(1f); - mView.setAlpha(1f); + if (!dismissed) { + mView.setAlpha(1f); + } mView.setTranslationX(initialPosition.x); mView.setTranslationY(initialPosition.y); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java index efc747c725..42bd19726d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java @@ -27,8 +27,8 @@ import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.FloatPropertyCompat; import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Controls bubble bar drag interactions. @@ -77,6 +77,8 @@ public class BubbleDragController { private BubbleBarPinController mBubbleBarPinController; private BubblePinController mBubblePinController; + private boolean mIsDragging; + public BubbleDragController(TaskbarActivityContext activity) { mActivity = activity; } @@ -153,6 +155,7 @@ public class BubbleDragController { @Override protected void onDragDismiss() { mBubblePinController.onDragEnd(); + mBubbleBarViewController.onBubbleDismissed(bubbleView); mBubbleBarViewController.onBubbleDragEnd(); } @@ -239,6 +242,16 @@ public class BubbleDragController { }); } + /** Whether there is an item being dragged or not. */ + public boolean isDragging() { + return mIsDragging; + } + + /** Sets whether something is being dragged or not. */ + public void setIsDragging(boolean isDragging) { + mIsDragging = isDragging; + } + /** * Bubble touch listener for handling a single bubble view or bubble bar view while dragging. * The dragging starts after "shorter" long click (the long click duration might change): @@ -435,6 +448,7 @@ public class BubbleDragController { private void startDragging(@NonNull View view) { onDragStart(); + BubbleDragController.this.setIsDragging(true); mActivity.setTaskbarWindowFullscreen(true); mAnimator = new BubbleDragAnimator(view); mAnimator.animateFocused(); @@ -451,6 +465,7 @@ public class BubbleDragController { } private void stopDragging(@NonNull View view, @NonNull MotionEvent event) { + BubbleDragController.this.setIsDragging(false); Runnable onComplete = () -> { mActivity.setTaskbarWindowFullscreen(false); cleanUp(view); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt index a77e685d00..af1666fa7d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt @@ -27,8 +27,9 @@ import android.view.View import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */ class BubblePinController( diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java deleted file mode 100644 index 74ddf90afc..0000000000 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java +++ /dev/null @@ -1,458 +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.bubbles; - -import static java.lang.Math.abs; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.annotation.Nullable; -import android.view.InsetsController; -import android.view.MotionEvent; -import android.view.View; - -import com.android.launcher3.R; -import com.android.launcher3.anim.AnimatedFloat; -import com.android.launcher3.taskbar.StashedHandleViewController; -import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.launcher3.taskbar.TaskbarControllers; -import com.android.launcher3.taskbar.TaskbarInsetsController; -import com.android.launcher3.taskbar.TaskbarStashController; -import com.android.launcher3.util.MultiPropertyFactory; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.shared.animation.PhysicsAnimator; - -/** - * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to - * create a cohesive animation between stashed/unstashed states. - */ -public class BubbleStashController { - - private static final String TAG = "BubbleStashController"; - - /** - * How long to stash/unstash. - */ - public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; - - /** - * The scale bubble bar animates to when being stashed. - */ - private static final float STASHED_BAR_SCALE = 0.5f; - - protected final TaskbarActivityContext mActivity; - - // Initialized in init. - private TaskbarControllers mControllers; - private TaskbarInsetsController mTaskbarInsetsController; - private BubbleBarViewController mBarViewController; - private BubbleStashedHandleViewController mHandleViewController; - private TaskbarStashController mTaskbarStashController; - - private MultiPropertyFactory.MultiProperty mIconAlphaForStash; - private AnimatedFloat mIconScaleForStash; - private AnimatedFloat mIconTranslationYForStash; - private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha; - - private boolean mRequestedStashState; - private boolean mRequestedExpandedState; - - private boolean mIsStashed; - private int mStashedHeight; - private int mUnstashedHeight; - private boolean mBubblesShowingOnHome; - private boolean mBubblesShowingOnOverview; - private boolean mIsSysuiLocked; - - private final float mHandleCenterFromScreenBottom; - - @Nullable - private AnimatorSet mAnimator; - - public BubbleStashController(TaskbarActivityContext activity) { - mActivity = activity; - // the handle is centered within the stashed taskbar area - mHandleCenterFromScreenBottom = - mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f; - } - - public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { - mControllers = controllers; - mTaskbarInsetsController = controllers.taskbarInsetsController; - mBarViewController = bubbleControllers.bubbleBarViewController; - mHandleViewController = bubbleControllers.bubbleStashedHandleViewController; - mTaskbarStashController = controllers.taskbarStashController; - - mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0); - mIconScaleForStash = mBarViewController.getBubbleBarScale(); - mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY(); - - mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get( - StashedHandleViewController.ALPHA_INDEX_STASHED); - - mStashedHeight = mHandleViewController.getStashedHeight(); - mUnstashedHeight = mHandleViewController.getUnstashedHeight(); - } - - /** - * Returns the touchable height of the bubble bar based on it's stashed state. - */ - public int getTouchableHeight() { - return mIsStashed ? mStashedHeight : mUnstashedHeight; - } - - /** - * Returns whether the bubble bar is currently stashed. - */ - public boolean isStashed() { - return mIsStashed; - } - - /** - * Animates the handle (or bubble bar depending on state) to be visible after the device is - * unlocked. - * - *

Normally either the bubble bar or the handle is visible, - * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition - * between these two states. But the transition from the state where both the bar and handle - * are invisible is slightly different. - */ - private void animateAfterUnlock() { - AnimatorSet animatorSet = new AnimatorSet(); - if (mBubblesShowingOnHome || mBubblesShowingOnOverview) { - mIsStashed = false; - animatorSet.playTogether(mIconScaleForStash.animateToValue(1), - mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()), - mIconAlphaForStash.animateToValue(1)); - } else { - mIsStashed = true; - animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1)); - } - - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - onIsStashedChanged(); - } - }); - animatorSet.setDuration(BAR_STASH_DURATION).start(); - } - - /** - * Called when launcher enters or exits the home page. Bubbles are unstashed on home. - */ - public void setBubblesShowingOnHome(boolean onHome) { - if (mBubblesShowingOnHome != onHome) { - mBubblesShowingOnHome = onHome; - - if (!mBarViewController.hasBubbles()) { - // if there are no bubbles, there's nothing to show, so just return. - return; - } - - if (mBubblesShowingOnHome) { - showBubbleBar(/* expanded= */ false); - // When transitioning from app to home the stash animator may already have been - // created, so we need to animate the bubble bar here to align with hotseat. - if (!mIsStashed) { - mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat()) - .start(); - } - // If the bubble bar is already unstashed, the taskbar touchable region won't be - // updated correctly, so force an update here. - mControllers.runAfterInit(() -> - mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); - } else if (!mBarViewController.isExpanded()) { - stashBubbleBar(); - } - } - } - - /** Whether bubbles are showing on the launcher home page. */ - public boolean isBubblesShowingOnHome() { - return mBubblesShowingOnHome; - } - - // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing - /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */ - public void setBubblesShowingOnOverview(boolean onOverview) { - if (mBubblesShowingOnOverview != onOverview) { - mBubblesShowingOnOverview = onOverview; - if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) { - stashBubbleBar(); - } else { - // When transitioning to overview the stash animator may already have been - // created, so we need to animate the bubble bar here to align with taskbar. - mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar()) - .start(); - } - } - } - - /** Whether bubbles are showing on Overview. */ - public boolean isBubblesShowingOnOverview() { - return mBubblesShowingOnOverview; - } - - /** Called when sysui locked state changes, when locked, bubble bar is stashed. */ - public void onSysuiLockedStateChange(boolean isSysuiLocked) { - if (isSysuiLocked != mIsSysuiLocked) { - mIsSysuiLocked = isSysuiLocked; - if (!mIsSysuiLocked && mBarViewController.hasBubbles()) { - animateAfterUnlock(); - } - } - } - - /** - * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the - * bar does not stash). - */ - public void stashBubbleBar() { - mRequestedStashState = true; - mRequestedExpandedState = false; - updateStashedAndExpandedState(); - } - - /** - * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}. - */ - public void showBubbleBar(boolean expandBubbles) { - mRequestedStashState = false; - mRequestedExpandedState = expandBubbles; - updateStashedAndExpandedState(); - } - - private void updateStashedAndExpandedState() { - if (mBarViewController.isHiddenForNoBubbles()) { - // If there are no bubbles the bar and handle are invisible, nothing to do here. - return; - } - boolean isStashed = mRequestedStashState - && !mBubblesShowingOnHome - && !mBubblesShowingOnOverview; - if (mIsStashed != isStashed) { - // notify the view controller that the stash state is about to change so that it can - // cancel an ongoing animation if there is one. - // note that this has to be called before updating mIsStashed with the new value, - // otherwise interrupting an ongoing animation may update it again with the wrong state - mBarViewController.onStashStateChanging(); - mIsStashed = isStashed; - if (mAnimator != null) { - mAnimator.cancel(); - } - mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION); - mAnimator.start(); - onIsStashedChanged(); - } - if (mBarViewController.isExpanded() != mRequestedExpandedState) { - mBarViewController.setExpanded(mRequestedExpandedState); - } - } - - /** - * Create a stash animation. - * - * @param isStashed whether it's a stash animation or an unstash animation - * @param duration duration of the animation - * @return the animation - */ - private AnimatorSet createStashAnimator(boolean isStashed, long duration) { - AnimatorSet animatorSet = new AnimatorSet(); - - AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); - // Not exactly half and may overlap. See [first|second]HalfDurationScale below. - AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); - AnimatorSet secondHalfAnimatorSet = new AnimatorSet(); - - final float firstHalfDurationScale; - final float secondHalfDurationScale; - - if (isStashed) { - firstHalfDurationScale = 0.75f; - secondHalfDurationScale = 0.5f; - - fullLengthAnimatorSet.play( - mIconTranslationYForStash.animateToValue(getStashTranslation())); - - firstHalfAnimatorSet.playTogether( - mIconAlphaForStash.animateToValue(0), - mIconScaleForStash.animateToValue(STASHED_BAR_SCALE)); - secondHalfAnimatorSet.playTogether( - mBubbleStashedHandleAlpha.animateToValue(1)); - } else { - firstHalfDurationScale = 0.5f; - secondHalfDurationScale = 0.75f; - - final float translationY = getBubbleBarTranslationY(); - - fullLengthAnimatorSet.playTogether( - mIconScaleForStash.animateToValue(1), - mIconTranslationYForStash.animateToValue(translationY)); - - firstHalfAnimatorSet.playTogether( - mBubbleStashedHandleAlpha.animateToValue(0) - ); - secondHalfAnimatorSet.playTogether( - mIconAlphaForStash.animateToValue(1) - ); - } - - fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed)); - - fullLengthAnimatorSet.setDuration(duration); - firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); - secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); - secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); - - animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, - secondHalfAnimatorSet); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimator = null; - mControllers.runAfterInit(() -> { - if (isStashed) { - mBarViewController.setExpanded(false); - } - mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); - }); - } - }); - return animatorSet; - } - - private float getStashTranslation() { - return (mUnstashedHeight - mStashedHeight) / 2f; - } - - private void onIsStashedChanged() { - mControllers.runAfterInit(() -> { - mHandleViewController.onIsStashedChanged(); - mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); - }); - } - - public float getBubbleBarTranslationYForTaskbar() { - return -mActivity.getDeviceProfile().taskbarBottomMargin; - } - - private float getBubbleBarTranslationYForHotseat() { - final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx; - final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx; - return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs( - hotseatCellHeight - mUnstashedHeight) / 2; - } - - public float getBubbleBarTranslationY() { - // If we're on home, adjust the translation so the bubble bar aligns with hotseat. - // Otherwise we're either showing in an app or in overview. In either case adjust it so - // the bubble bar aligns with the taskbar. - return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat() - : getBubbleBarTranslationYForTaskbar(); - } - - /** - * The difference on the Y axis between the center of the handle and the center of the bubble - * bar. - */ - public float getDiffBetweenHandleAndBarCenters() { - // the difference between the centers of the handle and the bubble bar is the difference - // between their distance from the bottom of the screen. - - float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f; - return mHandleCenterFromScreenBottom - barCenter; - } - - /** The distance the handle moves as part of the new bubble animation. */ - public float getStashedHandleTranslationForNewBubbleAnimation() { - // the should move up to the top of the stashed taskbar area. it is centered within it so - // it should move the same distance as it is away from the bottom. - return -mHandleCenterFromScreenBottom; - } - - /** Checks whether the motion event is over the stash handle. */ - public boolean isEventOverStashHandle(MotionEvent ev) { - return mHandleViewController.isEventOverHandle(ev); - } - - /** Set a bubble bar location */ - public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { - mHandleViewController.setBubbleBarLocation(bubbleBarLocation); - } - - /** Returns the [PhysicsAnimator] for the stashed handle view. */ - public PhysicsAnimator getStashedHandlePhysicsAnimator() { - return mHandleViewController.getPhysicsAnimator(); - } - - /** Notifies taskbar that it should update its touchable region. */ - public void updateTaskbarTouchRegion() { - mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); - } - - /** Shows the bubble bar immediately without animation. */ - public void showBubbleBarImmediate() { - mHandleViewController.setTranslationYForSwipe(0); - mIconTranslationYForStash.updateValue(getBubbleBarTranslationY()); - mIconAlphaForStash.setValue(1); - mIconScaleForStash.updateValue(1); - mIsStashed = false; - onIsStashedChanged(); - } - - /** Stashes the bubble bar immediately without animation. */ - public void stashBubbleBarImmediate() { - mHandleViewController.setTranslationYForSwipe(0); - mBubbleStashedHandleAlpha.setValue(1); - mIconAlphaForStash.setValue(0); - mIconTranslationYForStash.updateValue(getStashTranslation()); - mIconScaleForStash.updateValue(STASHED_BAR_SCALE); - mIsStashed = true; - onIsStashedChanged(); - } - - /** - * Updates the values of the internal animators after the new bubble animation was interrupted - * - * @param isStashed whether the current state should be stashed - * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the - * bubble bar is showing to ensure that the stash animator runs - * smoothly. - */ - public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) { - if (isStashed) { - mBubbleStashedHandleAlpha.setValue(1); - mIconAlphaForStash.setValue(0); - mIconScaleForStash.updateValue(STASHED_BAR_SCALE); - mIconTranslationYForStash.updateValue(getStashTranslation()); - } else { - mBubbleStashedHandleAlpha.setValue(0); - mHandleViewController.setTranslationYForSwipe(0); - mIconAlphaForStash.setValue(1); - mIconScaleForStash.updateValue(1); - mIconTranslationYForStash.updateValue(bubbleBarTranslationY); - } - mIsStashed = isStashed; - onIsStashedChanged(); - } - - /** Set the translation Y for the stashed handle. */ - public void setHandleTranslationY(float ty) { - mHandleViewController.setTranslationYForSwipe(ty); - } -} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java index 91103d75bd..3640c3b60e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java @@ -21,6 +21,7 @@ import static android.view.View.VISIBLE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.content.res.Resources; import android.graphics.Outline; import android.graphics.Rect; @@ -28,18 +29,19 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.taskbar.StashedHandleView; import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.Executors; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; /** * Handles properties/data collection, then passes the results to our stashed handle View to render. @@ -49,23 +51,29 @@ public class BubbleStashedHandleViewController { private final TaskbarActivityContext mActivity; private final StashedHandleView mStashedHandleView; private final MultiValueAlpha mStashedHandleAlpha; + private float mTranslationForSwipeY; + private float mTranslationForStashY; // Initialized in init. private BubbleBarViewController mBarViewController; private BubbleStashController mBubbleStashController; private RegionSamplingHelper mRegionSamplingHelper; - private int mBarSize; - private int mStashedTaskbarHeight; + // Height of the area for the stash handle. Handle will be drawn in the center of this. + // This is also the area where touch is handled on the handle. + private int mStashedBubbleBarHeight; private int mStashedHandleWidth; private int mStashedHandleHeight; - // The bounds we want to clip to in the settled state when showing the stashed handle. + // The bounds of the stashed handle in settled state. private final Rect mStashedHandleBounds = new Rect(); + private float mStashedHandleRadius; // When the reveal animation is cancelled, we can assume it's about to create a new animation, // which should start off at the same point the cancelled one left off. private float mStartProgressForNextRevealAnim; - private boolean mWasLastRevealAnimReversed; + // Use a nullable boolean to handle initial case where the last animation direction is not known + @Nullable + private Boolean mWasLastRevealAnimReversed = null; // XXX: if there are more of these maybe do state flags instead private boolean mHiddenForSysui; @@ -77,32 +85,39 @@ public class BubbleStashedHandleViewController { mActivity = activity; mStashedHandleView = stashedHandleView; mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1); + mStashedHandleAlpha.setUpdateVisibility(true); } - public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { + /** Initialize controller. */ + public void init(BubbleControllers bubbleControllers) { mBarViewController = bubbleControllers.bubbleBarViewController; mBubbleStashController = bubbleControllers.bubbleStashController; + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); Resources resources = mActivity.getResources(); mStashedHandleHeight = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_handle_height); mStashedHandleWidth = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_handle_width); - mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size); - final int bottomMargin = resources.getDimensionPixelSize( - R.dimen.transient_taskbar_bottom_margin); - mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin; + int barSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size); + // Use the max translation for bubble bar whether it is on the home screen or in app. + // Use values directly from device profile to avoid referencing other bubble controllers + // during init flow. + int maxTy = Math.max(deviceProfile.hotseatBarBottomSpacePx, + deviceProfile.taskbarBottomMargin); + // Adjust handle view size to accommodate the handle morphing into the bubble bar + mStashedHandleView.getLayoutParams().height = barSize + maxTy; mStashedHandleAlpha.get(0).setValue(0); - mStashedTaskbarHeight = resources.getDimensionPixelSize( + mStashedBubbleBarHeight = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_size); mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - float stashedHandleRadius = view.getHeight() / 2f; - outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius); + mStashedHandleRadius = view.getHeight() / 2f; + outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius); } }); @@ -117,7 +132,7 @@ public class BubbleStashedHandleViewController { public Rect getSampledRegion(View sampledView) { return mStashedHandleView.getSampledRegion(); } - }, Executors.UI_HELPER_EXECUTOR); + }, Executors.MAIN_EXECUTOR, Executors.UI_HELPER_EXECUTOR); mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> updateBounds(mBarViewController.getBubbleBarLocation())); @@ -131,28 +146,25 @@ public class BubbleStashedHandleViewController { private void updateBounds(BubbleBarLocation bubbleBarLocation) { // As more bubbles get added, the icon bounds become larger. To ensure a consistent // handle bar position, we pin it to the edge of the screen. - final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2; + final int stashedCenterY = mStashedHandleView.getHeight() - mStashedBubbleBarHeight / 2; + final int stashedCenterX; if (bubbleBarLocation.isOnLeft(mStashedHandleView.isLayoutRtl())) { final int left = mBarViewController.getHorizontalMargin(); - mStashedHandleBounds.set( - left, - stashedCenterY - mStashedHandleHeight / 2, - left + mStashedHandleWidth, - stashedCenterY + mStashedHandleHeight / 2); - mStashedHandleView.setPivotX(0); + stashedCenterX = left + mStashedHandleWidth / 2; } else { final int right = - mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin(); - mStashedHandleBounds.set( - right - mStashedHandleWidth, - stashedCenterY - mStashedHandleHeight / 2, - right, - stashedCenterY + mStashedHandleHeight / 2); - mStashedHandleView.setPivotX(mStashedHandleView.getWidth()); + mStashedHandleView.getRight() - mBarViewController.getHorizontalMargin(); + stashedCenterX = right - mStashedHandleWidth / 2; } - + mStashedHandleBounds.set( + stashedCenterX - mStashedHandleWidth / 2, + stashedCenterY - mStashedHandleHeight / 2, + stashedCenterX + mStashedHandleWidth / 2, + stashedCenterY + mStashedHandleHeight / 2 + ); mStashedHandleView.updateSampledRegion(mStashedHandleBounds); - mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f); + mStashedHandleView.setPivotX(stashedCenterX); + mStashedHandleView.setPivotY(stashedCenterY); } public void onDestroy() { @@ -160,6 +172,13 @@ public class BubbleStashedHandleViewController { mRegionSamplingHelper = null; } + /** + * Returns the width of the stashed handle. + */ + public int getStashedWidth() { + return mStashedHandleWidth; + } + /** * Returns the height of the stashed handle. */ @@ -168,10 +187,10 @@ public class BubbleStashedHandleViewController { } /** - * Returns the height when the bubble bar is unstashed (so the height of the bubble bar). + * Returns bounds of the stashed handle view */ - public int getUnstashedHeight() { - return mBarSize; + public void getBounds(Rect bounds) { + bounds.set(mStashedHandleBounds); } /** @@ -241,7 +260,25 @@ public class BubbleStashedHandleViewController { * Sets the translation of the stashed handle during the swipe up gesture. */ public void setTranslationYForSwipe(float transY) { - mStashedHandleView.setTranslationY(transY); + mTranslationForSwipeY = transY; + updateTranslationY(); + } + + /** + * Sets the translation of the stashed handle during the spring on stash animation. + */ + public void setTranslationYForStash(float transY) { + mTranslationForStashY = transY; + updateTranslationY(); + } + + private void updateTranslationY() { + mStashedHandleView.setTranslationY(mTranslationForSwipeY + mTranslationForStashY); + } + + /** Returns the translation of the stashed handle. */ + public float getTranslationY() { + return mStashedHandleView.getTranslationY(); } /** @@ -257,18 +294,17 @@ public class BubbleStashedHandleViewController { * the size of where the bubble bar icons will be. */ public Animator createRevealAnimToIsStashed(boolean isStashed) { - Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds()); + Rect bubbleBarBounds = getLocalBubbleBarBounds(); - // Account for the full visual height of the bubble bar - int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2; - bubbleBarBounds.top -= heightDiff; - bubbleBarBounds.bottom += heightDiff; - float stashedHandleRadius = mStashedHandleView.getHeight() / 2f; + float bubbleBarRadius = bubbleBarBounds.height() / 2f; final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider( - stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds); + bubbleBarRadius, mStashedHandleRadius, bubbleBarBounds, mStashedHandleBounds); boolean isReversed = !isStashed; - boolean changingDirection = mWasLastRevealAnimReversed != isReversed; + // We are only changing direction when mWasLastRevealAnimReversed is set at least once + boolean changingDirection = + mWasLastRevealAnimReversed != null && mWasLastRevealAnimReversed != isReversed; + mWasLastRevealAnimReversed = isReversed; if (changingDirection) { mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim; @@ -285,6 +321,21 @@ public class BubbleStashedHandleViewController { return revealAnim; } + /** + * Get bounds for the bubble bar in the space of the handle view + */ + private Rect getLocalBubbleBarBounds() { + // Position the bubble bar bounds to the space of handle view + Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds()); + // Start by moving bubble bar bounds to the bottom of handle view + int height = bubbleBarBounds.height(); + bubbleBarBounds.bottom = mStashedHandleView.getHeight(); + bubbleBarBounds.top = bubbleBarBounds.bottom - height; + // Then apply translation that is applied to the bubble bar + bubbleBarBounds.offset(0, (int) mBubbleStashController.getBubbleBarTranslationY()); + return bubbleBarBounds; + } + /** Checks that the stash handle is visible and that the motion event is within bounds. */ public boolean isEventOverHandle(MotionEvent ev) { if (mStashedHandleView.getVisibility() != VISIBLE) { @@ -292,15 +343,11 @@ public class BubbleStashedHandleViewController { } // the bounds of the handle only include the visible part, so we check that the Y coordinate - // is anywhere within the stashed taskbar height. - int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight; - - return (int) ev.getRawY() >= top && containsX((int) ev.getRawX()); - } - - /** Checks if the given x coordinate is within the stashed handle bounds. */ - public boolean containsX(int x) { - return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right; + // is anywhere within the stashed height of bubble bar (same as taskbar stashed height). + final int top = mActivity.getDeviceProfile().heightPx - mStashedBubbleBarHeight; + final float x = ev.getRawX(); + return ev.getRawY() >= top && x >= mStashedHandleBounds.left + && x <= mStashedHandleBounds.right; } /** Set a bubble bar location */ diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index 2f92fbbaba..561df5c2ee 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -16,26 +16,28 @@ package com.android.launcher3.taskbar.bubbles; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Outline; +import android.graphics.Color; +import android.graphics.Path; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; import com.android.launcher3.icons.DotRenderer; -import com.android.launcher3.icons.IconNormalizer; -import com.android.wm.shell.animation.Interpolators; - -import java.util.EnumSet; +import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleInfo; // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share. @@ -47,25 +49,9 @@ public class BubbleView extends ConstraintLayout { public static final int DEFAULT_PATH_SIZE = 100; - /** - * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or - * another. If any of these flags are set, the dot will not be shown. - * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown. - */ - enum SuppressionFlag { - // TODO: (b/277815200) implement flyout - // Suppressed because the flyout is visible - it will morph into the dot via animation. - FLYOUT_VISIBLE, - // Suppressed because this bubble is behind others in the collapsed stack. - BEHIND_STACK, - } - - private final EnumSet mSuppressionFlags = - EnumSet.noneOf(SuppressionFlag.class); - private final ImageView mBubbleIcon; private final ImageView mAppIcon; - private final int mBubbleSize; + private int mBubbleSize; private float mDragTranslationX; private float mOffsetX; @@ -82,11 +68,22 @@ public class BubbleView extends ConstraintLayout { // The current scale value of the dot private float mDotScale; + private boolean mProvideShadowOutline = true; + // TODO: (b/273310265) handle RTL // Whether the bubbles are positioned on the left or right side of the screen private boolean mOnLeft = false; private BubbleBarItem mBubble; + private boolean mIsOverflow; + + private Bitmap mIcon; + + @Nullable + private Controller mController; + + @Nullable + private BubbleBarBubbleIconsFactory mIconFactory = null; public BubbleView(Context context) { this(context, null); @@ -107,8 +104,6 @@ public class BubbleView extends ConstraintLayout { setLayoutDirection(LAYOUT_DIRECTION_LTR); LayoutInflater.from(context).inflate(R.layout.bubble_view, this); - - mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); mBubbleIcon = findViewById(R.id.icon_view); mAppIcon = findViewById(R.id.app_icon_view); @@ -116,18 +111,21 @@ public class BubbleView extends ConstraintLayout { setFocusable(true); setClickable(true); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - BubbleView.this.getOutline(outline); - } - }); + + // We manage the shadow ourselves when creating the bitmap + setOutlineAmbientShadowColor(Color.TRANSPARENT); + setOutlineSpotShadowColor(Color.TRANSPARENT); } - private void getOutline(Outline outline) { - final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize); - final int inset = (mBubbleSize - normalizedSize) / 2; - outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); + private void updateBubbleSizeAndDotRender() { + int updatedBubbleSize = Math.min(getWidth(), getHeight()); + if (updatedBubbleSize == mBubbleSize) return; + mBubbleSize = updatedBubbleSize; + mIconFactory = new BubbleBarBubbleIconsFactory(mContext, mBubbleSize); + updateBubbleIcon(); + if (mBubble == null || mBubble instanceof BubbleBarOverflow) return; + Path dotPath = ((BubbleBarBubble) mBubble).getDotPath(); + mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE); } /** @@ -163,6 +161,12 @@ public class BubbleView extends ConstraintLayout { setTranslationX(mDragTranslationX + mOffsetX); } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateBubbleSizeAndDotRender(); + } + @Override public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -181,11 +185,68 @@ public class BubbleView extends ConstraintLayout { mDotRenderer.draw(canvas, mDrawParams); } + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); + if (mBubble instanceof BubbleBarBubble) { + info.addAction(AccessibilityNodeInfo.ACTION_DISMISS); + } + if (mController != null) { + if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right, + getResources().getString(R.string.bubble_bar_action_move_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left, + getResources().getString(R.string.bubble_bar_action_move_left))); + } + } + } + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + if (mController != null) { + mController.collapse(); + } + return true; + } + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + if (mController != null) { + mController.dismiss(this); + } + return true; + } + if (action == R.id.action_move_left) { + if (mController != null) { + mController.updateBubbleBarLocation(BubbleBarLocation.LEFT); + } + } + if (action == R.id.action_move_right) { + if (mController != null) { + mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + } + } + return false; + } + + void setController(@Nullable Controller controller) { + mController = controller; + } + /** Sets the bubble being rendered in this view. */ public void setBubble(BubbleBarBubble bubble) { mBubble = bubble; - mBubbleIcon.setImageBitmap(bubble.getIcon()); - mAppIcon.setImageBitmap(bubble.getBadge()); + mIcon = bubble.getIcon(); + updateBubbleIcon(); + if (bubble.getInfo().showAppBadge()) { + mAppIcon.setImageBitmap(bubble.getBadge()); + } else { + mAppIcon.setVisibility(GONE); + } mDotColor = bubble.getDotColor(); mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE); String contentDesc = bubble.getInfo().getTitle(); @@ -200,6 +261,18 @@ public class BubbleView extends ConstraintLayout { setContentDescription(contentDesc); } + private void updateBubbleIcon() { + Bitmap icon = null; + if (mIcon != null) { + icon = mIcon; + if (mIconFactory != null) { + BitmapDrawable iconDrawable = new BitmapDrawable(getResources(), icon); + icon = mIconFactory.createShadowedIconBitmap(iconDrawable, /* scale = */ 1f); + } + } + mBubbleIcon.setImageBitmap(icon); + } + /** * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles * but does not represent app content, instead it shows recent bubbles that couldn't fit into @@ -208,11 +281,18 @@ public class BubbleView extends ConstraintLayout { */ public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { mBubble = overflow; - mBubbleIcon.setImageBitmap(bitmap); + mIsOverflow = true; + mIcon = bitmap; + updateBubbleIcon(); mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description)); } + /** Whether this view represents the overflow button. */ + public boolean isOverflow() { + return mIsOverflow; + } + /** Returns the bubble being rendered in this view. */ @Nullable public BubbleBarItem getBubble() { @@ -220,9 +300,9 @@ public class BubbleView extends ConstraintLayout { } void updateDotVisibility(boolean animate) { - final float targetScale = shouldDrawDot() ? 1f : 0f; + final float targetScale = hasUnseenContent() ? 1f : 0f; if (animate) { - animateDotScale(); + animateDotScale(targetScale); } else { mDotScale = targetScale; mAnimatingToDotScale = targetScale; @@ -230,73 +310,88 @@ public class BubbleView extends ConstraintLayout { } } - void updateBadgeVisibility() { - if (mBubble instanceof BubbleBarOverflow) { - // The overflow bubble does not have a badge, so just bail. + void setBadgeScale(float fraction) { + if (mAppIcon.getVisibility() == VISIBLE) { + mAppIcon.setScaleX(fraction); + mAppIcon.setScaleY(fraction); + } + } + + boolean hasUnseenContent() { + return mBubble != null + && mBubble instanceof BubbleBarBubble + && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); + } + + /** + * Used to determine if we can skip drawing frames. + * + *

Generally we should draw the dot when it is requested to be shown and there is unseen + * content. But when the dot is removed, we still want to draw frames so that it can be scaled + * out. + */ + private boolean shouldDrawDot() { + // if there's no dot there's nothing to draw, unless the dot was removed and we're in the + // middle of removing it + return hasUnseenContent() || mDotIsAnimating; + } + + /** Updates the dot scale to the specified fraction from 0 to 1. */ + private void setDotScale(float fraction) { + if (!shouldDrawDot()) { return; } - BubbleBarBubble bubble = (BubbleBarBubble) mBubble; - Bitmap appBadgeBitmap = bubble.getBadge(); - int translationX = mOnLeft - ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth()) - : 0; - mAppIcon.setTranslationX(translationX); - mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE); - } - - /** Sets whether this bubble is in the stack & not the first bubble. **/ - void setBehindStack(boolean behindStack, boolean animate) { - if (behindStack) { - mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK); - } else { - mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK); - } - updateDotVisibility(animate); - updateBadgeVisibility(); - } - - /** Whether this bubble is in the stack & not the first bubble. **/ - boolean isBehindStack() { - return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK); - } - - /** Whether the dot indicating unseen content in a bubble should be shown. */ - private boolean shouldDrawDot() { - boolean bubbleHasUnseenContent = mBubble != null - && mBubble instanceof BubbleBarBubble - && mSuppressionFlags.isEmpty() - && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); - - // Always render the dot if it's animating, since it could be animating out. Otherwise, show - // it if the bubble wants to show it, and we aren't suppressing it. - return bubbleHasUnseenContent || mDotIsAnimating; - } - - /** How big the dot should be, fraction from 0 to 1. */ - private void setDotScale(float fraction) { mDotScale = fraction; invalidate(); } - /** - * Animates the dot to the given scale. - */ - private void animateDotScale() { - float toScale = shouldDrawDot() ? 1f : 0f; - mDotIsAnimating = true; - - // Don't restart the animation if we're already animating to the given value. - if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { - mDotIsAnimating = false; + void showDotIfNeeded(float fraction) { + if (!hasUnseenContent()) { return; } + setDotScale(fraction); + } + void showDotIfNeeded(boolean animate) { + // only show the dot if we have unseen content + if (!hasUnseenContent()) { + return; + } + if (animate) { + animateDotScale(1f); + } else { + setDotScale(1f); + } + } + + void hideDot() { + animateDotScale(0f); + } + + /** Marks this bubble such that it no longer has unseen content, and hides the dot. */ + void markSeen() { + if (mBubble instanceof BubbleBarBubble bubble) { + BubbleInfo info = bubble.getInfo(); + info.setFlags( + info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); + hideDot(); + } + } + + /** Animates the dot to the given scale. */ + private void animateDotScale(float toScale) { + boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0; + + // Don't restart the animation if we're already animating to the given value or if the dot + // scale is not changing + if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) { + return; + } + mDotIsAnimating = true; mAnimatingToDotScale = toScale; final boolean showDot = toScale > 0f; - // Do NOT wait until after animation ends to setShowDot - // to avoid overriding more recent showDot states. clearAnimation(); animate() .setDuration(200) @@ -311,10 +406,24 @@ public class BubbleView extends ConstraintLayout { }).start(); } - @Override public String toString() { String toString = mBubble != null ? mBubble.getKey() : "null"; return "BubbleView{" + toString + "}"; } + + /** Interface for BubbleView to communicate with its controller */ + public interface Controller { + /** Get current bubble bar {@link BubbleBarLocation} */ + BubbleBarLocation getBubbleBarLocation(); + + /** This bubble should be dismissed */ + void dismiss(BubbleView bubble); + + /** Collapse the bubble bar */ + void collapse(); + + /** Request bubble bar location to be updated to the given location */ + void updateBubbleBarLocation(BubbleBarLocation location); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt new file mode 100644 index 0000000000..8af8ffb22a --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt @@ -0,0 +1,403 @@ +/* + * 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.Animator +import androidx.core.animation.ValueAnimator + +/** + * Animates individual bubbles within the bubble bar while the bubble bar is expanded. + * + * This class should only be kept for the duration of the animation and a new instance should be + * created for each animation. + */ +class BubbleAnimator( + private val iconSize: Float, + private val expandedBarIconSpacing: Float, + private val bubbleCount: Int, + private val onLeft: Boolean, +) { + + companion object { + const val ANIMATION_DURATION_MS = 250L + } + + private var state: State = State.Idle + private lateinit var animator: ValueAnimator + + fun animateNewBubble(selectedBubbleIndex: Int, listener: Listener) { + animator = createAnimator(listener) + state = State.AddingBubble(selectedBubbleIndex) + animator.start() + } + + fun animateRemovedBubble( + bubbleIndex: Int, + selectedBubbleIndex: Int, + removingLastBubble: Boolean, + listener: Listener + ) { + animator = createAnimator(listener) + state = State.RemovingBubble(bubbleIndex, selectedBubbleIndex, removingLastBubble) + animator.start() + } + + fun animateNewAndRemoveOld( + selectedBubbleIndex: Int, + removedBubbleIndex: Int, + listener: Listener + ) { + animator = createAnimator(listener) + state = + State.AddingAndRemoving( + selectedBubbleIndex = selectedBubbleIndex, + removedBubbleIndex = removedBubbleIndex + ) + animator.start() + } + + private fun createAnimator(listener: Listener): ValueAnimator { + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) + animator.addUpdateListener { animation -> + val animatedFraction = (animation as ValueAnimator).animatedFraction + listener.onAnimationUpdate(animatedFraction) + } + animator.addListener( + object : Animator.AnimatorListener { + + override fun onAnimationCancel(animation: Animator) { + listener.onAnimationCancel() + } + + override fun onAnimationEnd(animation: Animator) { + state = State.Idle + listener.onAnimationEnd() + } + + override fun onAnimationRepeat(animation: Animator) {} + + override fun onAnimationStart(animation: Animator) {} + } + ) + return animator + } + + /** + * The translation X of the bubble at index [bubbleIndex] when the bubble bar is expanded + * according to the progress of this animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getBubbleTranslationX(bubbleIndex: Int): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = animator.animatedFraction + ) + is State.RemovingBubble -> + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction + ) + is State.AddingAndRemoving -> + getBubbleTranslationXWhileAddingBubbleAtLimit( + bubbleIndex = bubbleIndex, + removedBubbleIndex = state.removedBubbleIndex, + addedBubbleScale = animator.animatedFraction, + removedBubbleScale = 1 - animator.animatedFraction + ) + } + } + + /** + * The expanded width of the bubble bar according to the progress of the animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getExpandedWidth(): Float { + val bubbleScale = + when (state) { + State.Idle -> 0f + is State.AddingBubble -> animator.animatedFraction + is State.RemovingBubble -> 1 - animator.animatedFraction + is State.AddingAndRemoving -> { + // since we're adding a bubble and removing another bubble, their sizes together + // equal to a single bubble. the width is the same as having bubbleCount - 1 + // bubbles at full scale. + val totalSpace = (bubbleCount - 2) * expandedBarIconSpacing + val totalIconSize = (bubbleCount - 1) * iconSize + return totalIconSize + totalSpace + } + } + // When this animator is running the bubble bar is expanded so it's safe to assume that we + // have at least 2 bubbles, but should update the logic to support optional overflow. + // If we're removing the last bubble, the entire bar should animate and we shouldn't get + // here. + val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing + val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize + return totalIconSize + totalSpace + } + + /** + * Returns the arrow position according to the progress of the animation and, if the selected + * bubble is being removed, accounting to the newly selected bubble. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getArrowPosition(): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> { + val tx = + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = animator.animatedFraction + ) + tx + iconSize / 2f + } + is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state) + is State.AddingAndRemoving -> { + // we never remove the selected bubble, so the arrow stays pointing to its center + val tx = + getBubbleTranslationXWhileAddingBubbleAtLimit( + bubbleIndex = state.selectedBubbleIndex, + removedBubbleIndex = state.removedBubbleIndex, + addedBubbleScale = animator.animatedFraction, + removedBubbleScale = 1 - animator.animatedFraction + ) + tx + iconSize / 2f + } + } + } + + private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float { + return if (state.selectedBubbleIndex != state.bubbleIndex) { + // if we're not removing the selected bubble, the selected bubble doesn't change so just + // return the translation X of the selected bubble and add half icon + val tx = + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction + ) + tx + iconSize / 2f + } else { + // we're removing the selected bubble so the arrow needs to point to a different bubble. + // if we're removing the last bubble the newly selected bubble will be the second to + // last. otherwise, it'll be the next bubble (closer to the overflow) + val iconAndSpacing = iconSize + expandedBarIconSpacing + if (state.removingLastBubble) { + if (onLeft) { + // the newly selected bubble is the bubble to the right. at the end of the + // animation all the bubbles will have shifted left, so the arrow stays at the + // same distance from the left edge of bar + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + } else { + // the newly selected bubble is the bubble to the left. at the end of the + // animation all the bubbles will have shifted right, and the arrow would + // eventually be closer to the left edge of the bar by iconAndSpacing + val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } + } else { + if (onLeft) { + // the newly selected bubble is to the left, and bubbles are shifting left, so + // move the arrow closer to the left edge of the bar by iconAndSpacing + val initialTx = + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } else { + // the newly selected bubble is to the right, and bubbles are shifting right, so + // the arrow stays at the same distance from the left edge of the bar + state.bubbleIndex * iconAndSpacing + iconSize / 2f + } + } + } + } + + /** + * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is + * expanded and a bubble is animating in or out. + * + * @param bubbleIndex the index of the bubble for which the translation is requested + * @param scalingBubbleIndex the index of the bubble that is animating + * @param bubbleScale the current scale of the animating bubble + */ + private fun getBubbleTranslationXWhileScalingBubble( + bubbleIndex: Int, + scalingBubbleIndex: Int, + bubbleScale: Float + ): Float { + val iconAndSpacing = iconSize + expandedBarIconSpacing + // the bubble is scaling from the center, so we need to adjust its translation so + // that the distance to the adjacent bubble scales at the same rate. + val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f + + return if (onLeft) { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the left and the current bubble is to the right of the scaling + // bubble so account for its scale + (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing + bubbleIndex == scalingBubbleIndex -> { + // the bar is on the left and this is the scaling bubble + val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize + // don't count the spacing between the scaling bubble and the bubble on the left + // because we need to scale that space + val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing + val scaledSpace = bubbleScale * expandedBarIconSpacing + totalIconSize + totalSpacing + scaledSpace + pivotAdjustment + } + else -> + // the bar is on the left and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + (bubbleCount - bubbleIndex - 1) * iconAndSpacing + } + } else { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the right and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + iconAndSpacing * bubbleIndex + bubbleIndex == scalingBubbleIndex -> + // the bar is on the right, and this is the animating bubble. it only needs to + // be adjusted for the scaling pivot. + iconAndSpacing * bubbleIndex + pivotAdjustment + else -> + // the bar is on the right and the scaling bubble is on the left so account for + // its scale + iconAndSpacing * (bubbleIndex - 1 + bubbleScale) + } + } + } + + private fun getBubbleTranslationXWhileAddingBubbleAtLimit( + bubbleIndex: Int, + removedBubbleIndex: Int, + addedBubbleScale: Float, + removedBubbleScale: Float + ): Float { + val iconAndSpacing = iconSize + expandedBarIconSpacing + // the bubbles are scaling from the center, so we need to adjust their translation so + // that the distance to the adjacent bubble scales at the same rate. + val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f + val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f + + return if (onLeft) { + // this is how many bubbles there are to the left of the current bubble. + // when the bubble bar is on the right the added bubble is the right-most bubble so it + // doesn't affect the translation of any other bubble. + // when the removed bubble is to the left of the current bubble, we need to subtract it + // from bubblesToLeft and use removedBubbleScale instead when calculating the + // translation. + val bubblesToLeft = bubbleCount - bubbleIndex - 1 + when { + bubbleIndex == 0 -> + // this is the added bubble and it's the right-most bubble. account for all the + // other bubbles -- including the removed bubble -- and adjust for the added + // bubble pivot. + (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing + + addedBubblePivotAdjustment + bubbleIndex < removedBubbleIndex -> + // the removed bubble is to the left so account for it + (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing + bubbleIndex == removedBubbleIndex -> { + // this is the removed bubble. all the bubbles to the left are at full scale + // but we need to scale the spacing between the removed bubble and the bubble to + // its left because the removed bubble disappears towards the left side + val totalIconSize = bubblesToLeft * iconSize + val totalSpacing = + (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing + totalIconSize + totalSpacing + removedBubblePivotAdjustment + } + else -> + // both added and removed bubbles are to the right so they don't affect the tx + bubblesToLeft * iconAndSpacing + } + } else { + when { + bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0 + bubbleIndex < removedBubbleIndex -> + // the bar is on the right and the removed bubble is on the right. the current + // bubble is unaffected by the removed bubble. only need to factor in the added + // bubble's scale. + iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) + bubbleIndex == removedBubbleIndex -> + // the bar is on the right, and this is the animating bubble. + iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) + + removedBubblePivotAdjustment + else -> + // both the added and the removed bubbles are to the left of the current bubble + iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale) + } + } + } + + val isRunning: Boolean + get() = state != State.Idle + + /** The state of the animation. */ + sealed interface State { + + /** The animation is not running. */ + data object Idle : State + + /** A new bubble is being added to the bubble bar. */ + data class AddingBubble(val selectedBubbleIndex: Int) : State + + /** A bubble is being removed from the bubble bar. */ + data class RemovingBubble( + /** The index of the bubble being removed. */ + val bubbleIndex: Int, + /** The index of the selected bubble. */ + val selectedBubbleIndex: Int, + /** Whether the bubble being removed is also the last bubble. */ + val removingLastBubble: Boolean + ) : State + + /** A new bubble is being added and an old bubble is being removed from the bubble bar. */ + data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) : + State + } + + /** Callbacks for the animation. */ + interface Listener { + + /** + * Notifies the listener of an animation update event, where `animatedFraction` represents + * the progress of the animation starting from 0 and ending at 1. + */ + fun onAnimationUpdate(animatedFraction: Float) + + /** Notifies the listener that the animation was canceled. */ + fun onAnimationCancel() + + /** Notifies that listener that the animation ended. */ + fun onAnimationEnd() + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt index d88e272332..6a955d92a0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -18,12 +18,16 @@ package com.android.launcher3.taskbar.bubbles.animation import android.view.View import android.view.View.VISIBLE +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ObjectAnimator import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.R import com.android.launcher3.taskbar.bubbles.BubbleBarBubble import com.android.launcher3.taskbar.bubbles.BubbleBarView -import com.android.launcher3.taskbar.bubbles.BubbleStashController import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController import com.android.wm.shell.shared.animation.PhysicsAnimator /** Handles animations for bubble bar bubbles. */ @@ -32,10 +36,21 @@ class BubbleBarViewAnimator constructor( private val bubbleBarView: BubbleBarView, private val bubbleStashController: BubbleStashController, + private val onExpanded: Runnable, private val scheduler: Scheduler = HandlerScheduler(bubbleBarView) ) { private var animatingBubble: AnimatingBubble? = null + private val bubbleBarBounceDistanceInPx = + bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance) + + fun hasAnimation() = animatingBubble != null + + val isAnimating: Boolean + get() { + val animatingBubble = animatingBubble ?: return false + return animatingBubble.state != AnimatingBubble.State.CREATED + } private companion object { /** The time to show the flyout. */ @@ -44,14 +59,41 @@ constructor( const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f /** The minimum alpha value to make the bubble bar touchable. */ const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f + /** The duration of the bounce animation. */ + const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L } /** Wrapper around the animating bubble with its show and hide animations. */ private data class AnimatingBubble( val bubbleView: BubbleView, val showAnimation: Runnable, - val hideAnimation: Runnable - ) + val hideAnimation: Runnable, + val expand: Boolean, + val state: State = State.CREATED + ) { + + /** + * The state of the animation. + * + * The animation is initially created but will be scheduled later using the [Scheduler]. + * + * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a + * transient state and eventually to animate out. + * + * However different events, such as touch and external signals, may cause the animation to + * end earlier. + */ + enum class State { + /** The animation is created but not started yet. */ + CREATED, + /** The bubble notification is animating in. */ + ANIMATING_IN, + /** The bubble notification is now fully showing and waiting to be hidden. */ + IN, + /** The bubble notification is animating out. */ + ANIMATING_OUT + } + } /** An interface for scheduling jobs. */ interface Scheduler { @@ -89,15 +131,18 @@ constructor( ) /** Animates a bubble for the state where the bubble bar is stashed. */ - fun animateBubbleInForStashed(b: BubbleBarBubble) { + fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() // the animation of a new bubble is divided into 2 parts. The first part shows the bubble // and the second part hides it after a delay. val showAnimation = buildHandleToBubbleBarAnimation() - val hideAnimation = buildBubbleBarToHandleAnimation() - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation() + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } @@ -117,39 +162,44 @@ constructor( * visible which helps avoiding further updates when we re-enter the second part. */ private fun buildHandleToBubbleBarAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation - bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.visibility = VISIBLE bubbleBarView.alpha = 0f bubbleBarView.translationY = 0f bubbleBarView.scaleX = 1f bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y + bubbleBarView.setBackgroundScaleX(1f) + bubbleBarView.setBackgroundScaleY(1f) bubbleBarView.relativePivotY = 0.5f // this is the offset between the center of the bubble bar and the center of the stash // handle. when the handle becomes invisible and we start animating in the bubble bar, // the translation y is offset by this value to make the transition from the handle to the // bar smooth. - val offset = bubbleStashController.diffBetweenHandleAndBarCenters + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() + val stashedHandleTranslationYForAnimation = + bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() val stashedHandleTranslationY = - bubbleStashController.stashedHandleTranslationForNewBubbleAnimation + bubbleStashController.getHandleTranslationY() ?: return@Runnable + val translationTracker = TranslationTracker(stashedHandleTranslationY) // this is the total distance that both the stashed handle and the bubble will be traveling // at the end of the animation the bubble bar will be positioned in the same place when it // shows while we're in an app. val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset - val animator = bubbleStashController.stashedHandlePhysicsAnimator + val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY) animator.addUpdateListener { handle, values -> val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener when { - ty >= stashedHandleTranslationY -> { + ty >= stashedHandleTranslationYForAnimation -> { // we're in the first leg of the animation. only animate the handle. the bubble // bar remains hidden during this part of the animation // map the path [0, stashedHandleTranslationY] to [0,1] - val fraction = ty / stashedHandleTranslationY + val fraction = ty / stashedHandleTranslationYForAnimation handle.alpha = 1 - fraction } ty >= totalTranslationY -> { @@ -163,8 +213,8 @@ constructor( if (bubbleBarView.alpha != 1f) { // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1] val fraction = - (ty - stashedHandleTranslationY) / - (totalTranslationY - stashedHandleTranslationY) + (ty - stashedHandleTranslationYForAnimation) / + (totalTranslationY - stashedHandleTranslationYForAnimation) bubbleBarView.alpha = fraction bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y + @@ -184,18 +234,16 @@ constructor( bubbleStashController.updateTaskbarTouchRegion() } } + translationTracker.updateTyAndExpandIfNeeded(ty) } animator.addEndListener { _, _, _, canceled, _, _, _ -> // if the show animation was canceled, also cancel the hide animation. this is typically // canceled in this class, but could potentially be canceled elsewhere. - if (canceled) { - val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener - scheduler.cancel(hideAnimation) - animatingBubble = null - bubbleBarView.onAnimatingBubbleCompleted() - bubbleBarView.relativePivotY = 1f + if (canceled || animatingBubble?.expand == true) { + cancelHideAnimation() return@addEndListener } + moveToState(AnimatingBubble.State.IN) // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } @@ -218,13 +266,14 @@ constructor( */ private fun buildBubbleBarToHandleAnimation() = Runnable { if (animatingBubble == null) return@Runnable - val offset = bubbleStashController.diffBetweenHandleAndBarCenters + moveToState(AnimatingBubble.State.ANIMATING_OUT) + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() val stashedHandleTranslationY = - bubbleStashController.stashedHandleTranslationForNewBubbleAnimation + bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() // this is the total distance that both the stashed handle and the bar will be traveling val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset bubbleStashController.setHandleTranslationY(totalTranslationY) - val animator = bubbleStashController.stashedHandlePhysicsAnimator + val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, 0f) animator.addUpdateListener { handle, values -> @@ -263,8 +312,8 @@ constructor( animator.addEndListener { _, _, _, canceled, _, _, _ -> animatingBubble = null if (!canceled) bubbleStashController.stashBubbleBarImmediate() - bubbleBarView.onAnimatingBubbleCompleted() bubbleBarView.relativePivotY = 1f + bubbleBarView.scaleY = 1f bubbleStashController.updateTaskbarTouchRegion() } animator.start() @@ -272,12 +321,14 @@ constructor( /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() // the animation of a new bubble is divided into 2 parts. The first part shows the bubble // and the second part hides it after a delay if we are in an app. - val showAnimation = buildBubbleBarBounceAnimation() + val showAnimation = buildBubbleBarSpringInAnimation() val hideAnimation = if (isInApp && !isExpanding) { buildBubbleBarToHandleAnimation() @@ -287,42 +338,101 @@ constructor( Runnable { animatingBubble = null bubbleStashController.showBubbleBarImmediate() - bubbleBarView.onAnimatingBubbleCompleted() bubbleStashController.updateTaskbarTouchRegion() } } - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } - private fun buildBubbleBarBounceAnimation() = Runnable { + private fun buildBubbleBarSpringInAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation - bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.translationY = bubbleBarView.height.toFloat() bubbleBarView.visibility = VISIBLE bubbleBarView.alpha = 1f bubbleBarView.scaleX = 1f bubbleBarView.scaleY = 1f + val translationTracker = TranslationTracker(bubbleBarView.translationY) + val animator = PhysicsAnimator.getInstance(bubbleBarView) animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) - animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() } + animator.addUpdateListener { _, values -> + val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener + translationTracker.updateTyAndExpandIfNeeded(ty) + bubbleStashController.updateTaskbarTouchRegion() + } animator.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + cancelHideAnimation() + } else { + moveToState(AnimatingBubble.State.IN) + } // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } animator.start() } + fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) { + // TODO b/346400677: handle animations for the same bubble interrupting each other + if (animatingBubble?.bubbleView?.bubble?.key == b.key) return + val bubbleView = b.view + val animator = PhysicsAnimator.getInstance(bubbleView) + if (animator.isRunning()) animator.cancel() + val showAnimation = buildBubbleBarBounceAnimation() + val hideAnimation = Runnable { + animatingBubble = null + bubbleStashController.showBubbleBarImmediate() + bubbleStashController.updateTaskbarTouchRegion() + } + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) + scheduler.post(showAnimation) + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + } + + /** + * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first + * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends + * the bubble bar moves back to its initial position with a spring animation. + */ + private fun buildBubbleBarBounceAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) + val ty = bubbleStashController.bubbleBarTranslationY + + val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView) + springBackAnimation.setDefaultSpringConfig(springConfig) + springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty) + springBackAnimation.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + expandBubbleBar() + cancelHideAnimation() + } else { + moveToState(AnimatingBubble.State.IN) + } + } + + // animate the bubble bar up and start the spring back down animation when it ends. + ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx) + .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS) + .withEndAction { + if (animatingBubble?.expand == true) expandBubbleBar() + springBackAnimation.start() + } + .start() + } + /** Handles touching the animating bubble bar. */ fun onBubbleBarTouchedWhileAnimating() { PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning() - bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning() + bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() val hideAnimation = animatingBubble?.hideAnimation ?: return scheduler.cancel(hideAnimation) - bubbleBarView.onAnimatingBubbleCompleted() bubbleBarView.relativePivotY = 1f animatingBubble = null } @@ -332,8 +442,7 @@ constructor( val hideAnimation = animatingBubble?.hideAnimation ?: return scheduler.cancel(hideAnimation) animatingBubble = null - bubbleStashController.stashedHandlePhysicsAnimator.cancel() - bubbleBarView.onAnimatingBubbleCompleted() + bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() bubbleBarView.relativePivotY = 1f bubbleStashController.onNewBubbleAnimationInterrupted( /* isStashed= */ bubbleBarView.alpha == 0f, @@ -341,7 +450,79 @@ constructor( ) } - private fun PhysicsAnimator.cancelIfRunning() { - if (isRunning()) cancel() + fun expandedWhileAnimating() { + val animatingBubble = animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(expand = true) + // if we're fully in and waiting to hide, cancel the hide animation and clean up + if (animatingBubble.state == AnimatingBubble.State.IN) { + expandBubbleBar() + cancelHideAnimation() + } + } + + private fun cancelHideAnimation() { + val hideAnimation = animatingBubble?.hideAnimation ?: return + scheduler.cancel(hideAnimation) + animatingBubble = null + bubbleBarView.relativePivotY = 1f + bubbleStashController.showBubbleBarImmediate() + } + + private fun PhysicsAnimator?.cancelIfRunning() { + if (this?.isRunning() == true) cancel() + } + + private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator { + setDuration(duration) + return this + } + + private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + endAction() + } + } + ) + return this + } + + private fun moveToState(state: AnimatingBubble.State) { + val animatingBubble = this.animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(state = state) + } + + private fun expandBubbleBar() { + bubbleBarView.isExpanded = true + onExpanded.run() + } + + /** + * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands + * as part of the animation, the expansion should start after the bubble bar reaches the peak + * position. + */ + private inner class TranslationTracker(initialTy: Float) { + private var previousTy = initialTy + private var startedExpanding = false + private var reachedPeak = false + + fun updateTyAndExpandIfNeeded(ty: Float) { + if (!reachedPeak) { + // the bubble bar is positioned at the bottom of the screen and moves up using + // negative ty values. the peak is reached the first time we see a value that is + // greater than the previous. + if (ty > previousTy) { + reachedPeak = true + } + } + val expand = animatingBubble?.expand ?: false + if (reachedPeak && expand && !startedExpanding) { + expandBubbleBar() + startedExpanding = true + } + previousTy = ty + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt new file mode 100644 index 0000000000..4939c993e0 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt @@ -0,0 +1,59 @@ +/* + * 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.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import com.android.launcher3.R + +/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */ +class BubbleBarFlyoutController( + private val container: FrameLayout, + private val positioner: BubbleBarFlyoutPositioner, +) { + + private var flyout: BubbleBarFlyoutView? = null + private val horizontalMargin = + container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) + + fun setUpFlyout(message: BubbleBarFlyoutMessage) { + flyout?.let(container::removeView) + val flyout = BubbleBarFlyoutView(container.context, onLeft = positioner.isOnLeft) + + flyout.translationY = positioner.targetTy + + val lp = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM or if (positioner.isOnLeft) Gravity.LEFT else Gravity.RIGHT, + ) + lp.marginStart = horizontalMargin + lp.marginEnd = horizontalMargin + container.addView(flyout, lp) + + flyout.setData(message) + this.flyout = flyout + } + + fun hideFlyout() { + val flyout = this.flyout ?: return + container.removeView(flyout) + this.flyout = null + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt new file mode 100644 index 0000000000..7298297d5c --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles.flyout + +import android.graphics.drawable.Drawable + +data class BubbleBarFlyoutMessage( + val senderAvatar: Drawable?, + val senderName: CharSequence, + val message: CharSequence, + val isGroupChat: Boolean, +) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt new file mode 100644 index 0000000000..deed1f5900 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt @@ -0,0 +1,27 @@ +/* + * 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 + +/** Provides positioning data to the flyout view. */ +interface BubbleBarFlyoutPositioner { + + /** Whether the flyout view should be positioned on left or the right edge. */ + val isOnLeft: Boolean + + /** The target translation Y that the flyout view should have when displayed. */ + val targetTy: Float +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt new file mode 100644 index 0000000000..4b91f461e9 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt @@ -0,0 +1,195 @@ +/* + * 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.content.res.Configuration +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.view.LayoutInflater +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.launcher3.R +import com.android.launcher3.popup.RoundedArrowDrawable + +/** The flyout view used to notify the user of a new bubble notification. */ +class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) : + ConstraintLayout(context) { + + private val sender: TextView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_name) } + + private val avatar: ImageView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_avatar) } + + private val message: TextView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_text) } + + private val flyoutPadding by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding) + } + + private val triangleHeight by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_height) + } + + private val triangleOverlap by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize( + R.dimen.bubblebar_flyout_triangle_overlap_amount + ) + } + + private val triangleWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_width) + } + + private val triangleRadius by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_radius) + } + + private val minFlyoutWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_min_width) + } + + private val maxFlyoutWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_max_width) + } + + private val cornerRadius: Float + private val triangle: Path = Path() + private var backgroundColor = Color.BLACK + + /** + * The paint used to draw the background, whose color changes as the flyout transitions to the + * tinted notification dot. + */ + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + + init { + LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true) + + val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.dialogCornerRadius)) + cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat() + ta.recycle() + + setWillNotDraw(false) + clipChildren = false + clipToPadding = false + + val padding = context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding) + // add extra padding to the bottom of the view to include the triangle + setPadding(padding, padding, padding, padding + triangleHeight - triangleOverlap) + translationZ = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_elevation).toFloat() + + RoundedArrowDrawable.addDownPointingRoundedTriangleToPath( + triangleWidth.toFloat(), + triangleHeight.toFloat(), + triangleRadius.toFloat(), + triangle, + ) + + applyConfigurationColors(resources.configuration) + } + + fun setData(flyoutMessage: BubbleBarFlyoutMessage) { + // the avatar is only displayed in group chat messages + if (flyoutMessage.senderAvatar != null && flyoutMessage.isGroupChat) { + avatar.visibility = VISIBLE + avatar.setImageDrawable(flyoutMessage.senderAvatar) + } else { + avatar.visibility = GONE + } + + val minTextViewWidth: Int + val maxTextViewWidth: Int + if (avatar.visibility == VISIBLE) { + minTextViewWidth = minFlyoutWidth - avatar.width - flyoutPadding * 2 + maxTextViewWidth = maxFlyoutWidth - avatar.width - flyoutPadding * 2 + } else { + // when there's no avatar, the width of the text view is constant, so we're setting the + // min and max to the same value + minTextViewWidth = minFlyoutWidth - flyoutPadding * 2 + maxTextViewWidth = minTextViewWidth + } + + if (flyoutMessage.senderName.isEmpty()) { + sender.visibility = GONE + } else { + sender.minWidth = minTextViewWidth + sender.maxWidth = maxTextViewWidth + sender.text = flyoutMessage.senderName + sender.visibility = VISIBLE + } + + message.minWidth = minTextViewWidth + message.maxWidth = maxTextViewWidth + message.text = flyoutMessage.message + } + + override fun onDraw(canvas: Canvas) { + canvas.drawRoundRect( + 0f, + 0f, + width.toFloat(), + height.toFloat() - triangleHeight + triangleOverlap, + cornerRadius, + cornerRadius, + backgroundPaint, + ) + drawTriangle(canvas) + super.onDraw(canvas) + } + + private fun drawTriangle(canvas: Canvas) { + canvas.save() + val triangleX = if (onLeft) cornerRadius else width - cornerRadius - triangleWidth + canvas.translate(triangleX, (height - triangleHeight).toFloat()) + canvas.drawPath(triangle, backgroundPaint) + canvas.restore() + } + + private fun applyConfigurationColors(configuration: Configuration) { + val nightModeFlags = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val isNightModeOn = nightModeFlags == Configuration.UI_MODE_NIGHT_YES + val defaultBackgroundColor = if (isNightModeOn) Color.BLACK else Color.WHITE + val defaultTextColor = if (isNightModeOn) Color.WHITE else Color.BLACK + val ta = + context.obtainStyledAttributes( + intArrayOf( + com.android.internal.R.attr.materialColorSurfaceContainer, + com.android.internal.R.attr.materialColorOnSurface, + com.android.internal.R.attr.materialColorOnSurfaceVariant, + ) + ) + backgroundColor = ta.getColor(0, defaultBackgroundColor) + sender.setTextColor(ta.getColor(1, defaultTextColor)) + message.setTextColor(ta.getColor(2, defaultTextColor)) + ta.recycle() + backgroundPaint.color = backgroundColor + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt new file mode 100644 index 0000000000..9721792540 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt @@ -0,0 +1,189 @@ +/* + * 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.graphics.Rect +import android.view.InsetsController +import android.view.MotionEvent +import android.view.View +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.BubbleStashedHandleViewController +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import java.io.PrintWriter + +/** StashController that defines stashing behaviour for the taskbar modes. */ +interface BubbleStashController { + + /** + * Abstraction on the task bar activity context to only provide the dimensions required for + * [BubbleBarView] translation Y computation. + */ + interface TaskbarHotseatDimensionsProvider { + + /** Provides taskbar bottom space in pixels. */ + fun getTaskbarBottomSpace(): Int + + /** Provides taskbar height in pixels. */ + fun getTaskbarHeight(): Int + + /** Provides hotseat bottom space in pixels. */ + fun getHotseatBottomSpace(): Int + + /** Provides hotseat height in pixels. */ + fun getHotseatHeight(): Int + } + + /** Execute passed action only after controllers are initiated. */ + interface ControllersAfterInitAction { + /** Execute action after controllers are initiated. */ + fun runAfterInit(action: Runnable) + } + + /** Whether bubble bar is currently stashed */ + val isStashed: Boolean + + /** Whether launcher enters or exits the home page. */ + var isBubblesShowingOnHome: Boolean + + /** Whether launcher enters or exits the overview page. */ + var isBubblesShowingOnOverview: Boolean + + /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */ + var isSysuiLocked: Boolean + + /** Whether there is a transient taskbar mode */ + val isTransientTaskBar: Boolean + + /** Whether stash control has a handle view */ + val hasHandleView: Boolean + + /** Initialize controller */ + fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction + ) + + /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */ + fun showBubbleBarImmediate() + + /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */ + fun showBubbleBarImmediate(bubbleBarTranslationY: Float) + + /** Stashes the bubble bar immediately without animation. */ + fun stashBubbleBarImmediate() + + /** Returns the touchable height of the bubble bar based on it's stashed state. */ + fun getTouchableHeight(): Int + + /** Whether bubble bar is currently visible */ + fun isBubbleBarVisible(): Boolean + + /** + * Updates the values of the internal animators after the new bubble animation was interrupted + * + * @param isStashed whether the current state should be stashed + * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the + * bubble bar is showing to ensure that the stash animator runs smoothly. + */ + fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) + + /** Checks whether the motion event is over the stash handle or bubble bar. */ + fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean + + /** Set a bubble bar location */ + fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) + + /** + * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded + * bubble bar based on the controller implementation. + */ + fun stashBubbleBar() + + /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */ + fun showBubbleBar(expandBubbles: Boolean) + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** + * The difference on the Y axis between the center of the handle and the center of the bubble + * bar. + */ + fun getDiffBetweenHandleAndBarCenters(): Float + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** The distance the handle moves as part of the new bubble animation. */ + fun getStashedHandleTranslationForNewBubbleAnimation(): Float + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Returns the [PhysicsAnimator] for the stashed handle view. */ + fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Notifies taskbar that it should update its touchable region. */ + fun updateTaskbarTouchRegion() + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Set the translation Y for the stashed handle. */ + fun setHandleTranslationY(translationY: Float) + + /** Returns the translation of the handle. */ + fun getHandleTranslationY(): Float? + + /** Returns bounds of the handle */ + fun getHandleBounds(bounds: Rect) + + /** + * Returns bubble bar Y position according to [isBubblesShowingOnHome] and + * [isBubblesShowingOnOverview] values. Default implementation only analyse + * [isBubblesShowingOnHome] and return translationY to align with the hotseat vertical center. + * For Other cases align bubbles with the taskbar. + */ + val bubbleBarTranslationY: Float + get() = + if (isBubblesShowingOnHome) { + bubbleBarTranslationYForHotseat + } else { + bubbleBarTranslationYForTaskbar + } + + /** Translation Y to align the bubble bar with the hotseat. */ + val bubbleBarTranslationYForTaskbar: Float + + /** Return translation Y to align the bubble bar with the taskbar. */ + val bubbleBarTranslationYForHotseat: Float + + /** Dumps the state of BubbleStashController. */ + fun dump(pw: PrintWriter) { + pw.println("Bubble stash controller state:") + pw.println(" isStashed: $isStashed") + pw.println(" isBubblesShowingOnOverview: $isBubblesShowingOnOverview") + pw.println(" isBubblesShowingOnHome: $isBubblesShowingOnHome") + pw.println(" isSysuiLocked: $isSysuiLocked") + } + + companion object { + /** How long to stash/unstash. */ + const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong() + + /** How long to translate Y coordinate of the BubbleBar. */ + const val BAR_TRANSLATION_DURATION = 300L + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt new file mode 100644 index 0000000000..a55763b417 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles.stashing + +import com.android.launcher3.DeviceProfile +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider + +/** + * Implementation of the [TaskbarHotseatDimensionsProvider] that take sizes from the + * [DeviceProfile]. + */ +class DeviceProfileDimensionsProviderAdapter( + private val taskbarActivityContext: TaskbarActivityContext +) : TaskbarHotseatDimensionsProvider { + override fun getTaskbarBottomSpace(): Int = deviceProfile().taskbarBottomMargin + + override fun getTaskbarHeight(): Int = deviceProfile().taskbarHeight + + override fun getHotseatBottomSpace(): Int = deviceProfile().hotseatBarBottomSpacePx + + override fun getHotseatHeight(): Int = deviceProfile().hotseatCellHeightPx + + private fun deviceProfile(): DeviceProfile = taskbarActivityContext.deviceProfile +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt new file mode 100644 index 0000000000..7d6f7adbbf --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles.stashing + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider +import com.android.launcher3.util.MultiPropertyFactory +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +class PersistentBubbleStashController( + private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider, +) : BubbleStashController { + + private lateinit var taskbarInsetsController: TaskbarInsetsController + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat + private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory.MultiProperty + private lateinit var bubbleBarScaleAnimator: AnimatedFloat + private lateinit var controllersAfterInitAction: ControllersAfterInitAction + + override var isBubblesShowingOnHome: Boolean = false + set(onHome) { + if (field == onHome) return + field = onHome + if (!bubbleBarViewController.hasBubbles()) { + // if there are no bubbles, there's nothing to show, so just return. + return + } + if (onHome) { + // When transition to home we should show collapse the bubble bar + updateExpandedState(expand = false) + } + animateBubbleBarY() + bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true) + } + + override var isBubblesShowingOnOverview: Boolean = false + set(onOverview) { + if (field == onOverview) return + field = onOverview + if (!onOverview) { + // When transition from overview we should show collapse the bubble bar + updateExpandedState(expand = false) + } + bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true) + } + + override var isSysuiLocked: Boolean = false + set(isLocked) { + if (field == isLocked) return + field = isLocked + if (!isLocked && bubbleBarViewController.hasBubbles()) { + animateAfterUnlock() + } + } + + override var isTransientTaskBar: Boolean = false + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override val hasHandleView: Boolean = false + + /** For persistent task bar we never stash the bubble bar */ + override val isStashed: Boolean = false + + override val bubbleBarTranslationYForTaskbar: Float + get() { + val taskbarBottomMargin = taskbarHotseatDimensionsProvider.getTaskbarBottomSpace() + val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight + val taskbarHeight = taskbarHotseatDimensionsProvider.getTaskbarHeight() + return -taskbarBottomMargin - (taskbarHeight - bubbleBarHeight) / 2f + } + + override val bubbleBarTranslationYForHotseat: Float + get() { + val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace() + val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight() + val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight + return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2 + } + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction + ) { + this.taskbarInsetsController = taskbarInsetsController + this.bubbleBarViewController = bubbleBarViewController + this.controllersAfterInitAction = controllersAfterInitAction + bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY + // bubble bar has only alpha property, getting it at index 0 + bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0) + bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScaleY + } + + private fun animateAfterUnlock() { + val animatorSet = AnimatorSet() + if (isBubblesShowingOnHome || isBubblesShowingOnOverview) { + animatorSet.playTogether( + bubbleBarScaleAnimator.animateToValue(1f), + bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY), + bubbleBarAlphaAnimator.animateToValue(1f) + ) + } + updateTouchRegionOnAnimationEnd(animatorSet) + animatorSet.setDuration(BAR_STASH_DURATION).start() + } + + override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY) + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY) + bubbleBarAlphaAnimator.setValue(1f) + bubbleBarScaleAnimator.updateValue(1f) + } + + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) { + // When the bubble bar is shown for the persistent task bar, there is no handle view, so no + // operation is performed. + } + + override fun stashBubbleBar() { + updateExpandedState(expand = false) + } + + override fun showBubbleBar(expandBubbles: Boolean) { + updateExpandedState(expandBubbles) + } + + override fun stashBubbleBarImmediate() { + // When the bubble bar is shown for the persistent task bar, there is no handle view, so no + // operation is performed. + } + + /** If bubble bar is visible return bubble bar height, 0 otherwise */ + override fun getTouchableHeight() = + if (isBubbleBarVisible()) { + bubbleBarViewController.bubbleBarCollapsedHeight.toInt() + } else { + 0 + } + + override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() + + override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) { + showBubbleBarImmediate(bubbleBarTranslationY) + } + + override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean = + bubbleBarViewController.isEventOverAnyItem(ev) + + override fun getDiffBetweenHandleAndBarCenters(): Float { + // distance from the bottom of the screen and the bubble bar center. + return -bubbleBarViewController.bubbleBarCollapsedHeight / 2f + } + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override fun getStashedHandleTranslationForNewBubbleAnimation(): Float = 0f + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? = null + + override fun updateTaskbarTouchRegion() { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + /** + * When the bubble bar is shown for the persistent task bar the bar does not stash, so no + * operation is performed + */ + override fun setHandleTranslationY(translationY: Float) { + // no op since does not have a handle view + } + + override fun getHandleTranslationY(): Float? = null + + override fun getHandleBounds(bounds: Rect) { + // no op since does not have a handle view + } + + private fun updateExpandedState(expand: Boolean) { + if (bubbleBarViewController.isHiddenForNoBubbles) { + // If there are no bubbles the bar is invisible, nothing to do here. + return + } + if (bubbleBarViewController.isExpanded != expand) { + bubbleBarViewController.isExpanded = expand + } + } + + /** Animates bubble bar Y accordingly to the showing mode */ + private fun animateBubbleBarY() { + val animator = + bubbleBarViewController.bubbleBarTranslationY.animateToValue(bubbleBarTranslationY) + updateTouchRegionOnAnimationEnd(animator) + animator.setDuration(BAR_TRANSLATION_DURATION) + animator.start() + } + + private fun updateTouchRegionOnAnimationEnd(animator: Animator) { + animator.addListener( + object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator) { + controllersAfterInitAction.runAfterInit { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + } + } + ) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt new file mode 100644 index 0000000000..4f0337ddc1 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt @@ -0,0 +1,521 @@ +/* + * 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.Animator +import android.animation.AnimatorSet +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart +import androidx.dynamicanimation.animation.SpringForce +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.R +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.anim.SpringAnimationBuilder +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_DURATION +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider +import com.android.launcher3.util.MultiPropertyFactory +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import kotlin.math.max + +class TransientBubbleStashController( + private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider, + private val context: Context, +) : BubbleStashController { + + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var taskbarInsetsController: TaskbarInsetsController + private lateinit var controllersAfterInitAction: ControllersAfterInitAction + + // stash view properties + private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null + private var stashHandleViewAlpha: MultiPropertyFactory.MultiProperty? = null + private var translationYDuringStash = AnimatedFloat { transY -> + bubbleStashedHandleViewController?.setTranslationYForStash(transY) + bubbleBarViewController.setTranslationYForStash(transY) + } + private val stashHandleStashVelocity = + context.resources.getDimension(R.dimen.bubblebar_stashed_handle_spring_velocity_dp_per_s) + private var stashedHeight: Int = 0 + + // bubble bar properties + private lateinit var bubbleBarAlpha: MultiPropertyFactory.MultiProperty + private lateinit var bubbleBarBubbleAlpha: AnimatedFloat + private lateinit var bubbleBarBackgroundAlpha: AnimatedFloat + private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat + private lateinit var bubbleBarBubbleTranslationY: AnimatedFloat + private lateinit var bubbleBarBackgroundScaleX: AnimatedFloat + private lateinit var bubbleBarBackgroundScaleY: AnimatedFloat + private val handleCenterFromScreenBottom = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f + + private var animator: AnimatorSet? = null + + override var isStashed: Boolean = false + @VisibleForTesting set + + override var isBubblesShowingOnHome: Boolean = false + set(onHome) { + if (field == onHome) return + field = onHome + if (!bubbleBarViewController.hasBubbles()) { + // if there are no bubbles, there's nothing to show, so just return. + return + } + if (onHome) { + updateStashedAndExpandedState(stash = false, expand = false) + // When transitioning from app to home we need to animate the bubble bar + // here to align with hotseat center. + animateBubbleBarYToHotseat() + } else if (!bubbleBarViewController.isExpanded) { + updateStashedAndExpandedState(stash = true, expand = false) + } + bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true) + } + + override var isBubblesShowingOnOverview: Boolean = false + set(onOverview) { + if (field == onOverview) return + field = onOverview + if (onOverview) { + // When transitioning to overview we need to animate the bubble bar to align with + // the taskbar bottom. + animateBubbleBarYToTaskbar() + } else { + updateStashedAndExpandedState(stash = true, expand = false) + } + bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true) + } + + override var isSysuiLocked: Boolean = false + set(isLocked) { + if (field == isLocked) return + field = isLocked + if (!isLocked && bubbleBarViewController.hasBubbles()) { + animateAfterUnlock() + } + } + + override val isTransientTaskBar: Boolean = true + + override val bubbleBarTranslationYForHotseat: Float + get() { + val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace() + val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight() + val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight + return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2 + } + + override val bubbleBarTranslationYForTaskbar: Float = + -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat() + + /** Check if we have handle view controller */ + override val hasHandleView: Boolean + get() = bubbleStashedHandleViewController != null + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction, + ) { + this.taskbarInsetsController = taskbarInsetsController + this.bubbleBarViewController = bubbleBarViewController + this.bubbleStashedHandleViewController = bubbleStashedHandleViewController + this.controllersAfterInitAction = controllersAfterInitAction + bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY + bubbleBarBubbleTranslationY = bubbleBarViewController.bubbleOffsetY + // bubble bar has only alpha property, getting it at index 0 + bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0) + bubbleBarBubbleAlpha = bubbleBarViewController.bubbleBarBubbleAlpha + bubbleBarBackgroundAlpha = bubbleBarViewController.bubbleBarBackgroundAlpha + bubbleBarBackgroundScaleX = bubbleBarViewController.bubbleBarBackgroundScaleX + bubbleBarBackgroundScaleY = bubbleBarViewController.bubbleBarBackgroundScaleY + stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0 + stashHandleViewAlpha = bubbleStashedHandleViewController?.stashedHandleAlpha?.get(0) + } + + private fun animateAfterUnlock() { + val animatorSet = AnimatorSet() + if (isBubblesShowingOnHome || isBubblesShowingOnOverview) { + isStashed = false + animatorSet.playTogether( + bubbleBarBackgroundScaleX.animateToValue(1f), + bubbleBarBackgroundScaleY.animateToValue(1f), + bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY), + bubbleBarAlpha.animateToValue(1f), + bubbleBarBubbleAlpha.animateToValue(1f), + bubbleBarBackgroundAlpha.animateToValue(1f), + ) + } else { + isStashed = true + stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) } + } + animatorSet.updateTouchRegionOnAnimationEnd().setDuration(BAR_STASH_DURATION).start() + } + + override fun showBubbleBarImmediate() { + showBubbleBarImmediate(bubbleBarTranslationY) + } + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + bubbleStashedHandleViewController?.setTranslationYForSwipe(0f) + stashHandleViewAlpha?.value = 0f + this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY) + bubbleBarAlpha.setValue(1f) + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + bubbleBarBackgroundScaleX.updateValue(1f) + bubbleBarBackgroundScaleY.updateValue(1f) + isStashed = false + onIsStashedChanged() + } + + override fun stashBubbleBarImmediate() { + bubbleStashedHandleViewController?.setTranslationYForSwipe(0f) + stashHandleViewAlpha?.value = 1f + this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation()) + bubbleBarAlpha.setValue(0f) + // Reset bubble and background alpha to 1 and only keep the bubble bar alpha at 0 + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + bubbleBarBackgroundScaleX.updateValue(getStashScaleX()) + bubbleBarBackgroundScaleY.updateValue(getStashScaleY()) + isStashed = true + onIsStashedChanged() + } + + override fun getTouchableHeight(): Int = + when { + isStashed -> stashedHeight + isBubbleBarVisible() -> bubbleBarViewController.bubbleBarCollapsedHeight.toInt() + else -> 0 + } + + override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() && !isStashed + + override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) = + if (isStashed) { + stashBubbleBarImmediate() + } else { + showBubbleBarImmediate(bubbleBarTranslationY) + } + + /** Check if [ev] belongs to the stash handle or the bubble bar views. */ + override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean { + val isOverHandle = bubbleStashedHandleViewController?.isEventOverHandle(ev) ?: false + return isOverHandle || bubbleBarViewController.isEventOverAnyItem(ev) + } + + /** Set the bubble bar stash handle location . */ + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) { + bubbleStashedHandleViewController?.setBubbleBarLocation(bubbleBarLocation) + } + + override fun stashBubbleBar() { + updateStashedAndExpandedState(stash = true, expand = false) + } + + override fun showBubbleBar(expandBubbles: Boolean) { + updateStashedAndExpandedState(stash = false, expandBubbles) + } + + override fun getDiffBetweenHandleAndBarCenters(): Float { + // the difference between the centers of the handle and the bubble bar is the difference + // between their distance from the bottom of the screen. + val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f + return handleCenterFromScreenBottom - barCenter + } + + override fun getStashedHandleTranslationForNewBubbleAnimation(): Float { + return -handleCenterFromScreenBottom + } + + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? { + return bubbleStashedHandleViewController?.physicsAnimator + } + + override fun updateTaskbarTouchRegion() { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + override fun setHandleTranslationY(translationY: Float) { + bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY) + } + + override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY + + override fun getHandleBounds(bounds: Rect) { + bubbleStashedHandleViewController?.getBounds(bounds) + } + + private fun getStashTranslation(): Float { + return (bubbleBarTranslationY - stashedHeight) / 2f + } + + @VisibleForTesting + fun getStashScaleX(): Float { + val handleWidth = bubbleStashedHandleViewController?.stashedWidth ?: 0 + return handleWidth / bubbleBarViewController.bubbleBarCollapsedWidth + } + + @VisibleForTesting + fun getStashScaleY(): Float { + val handleHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0 + return handleHeight / bubbleBarViewController.bubbleBarCollapsedHeight + } + + /** + * Create a stash animation. + * + * @param isStashed whether it's a stash animation or an unstash animation + * @param duration duration of the animation + * @return the animation + */ + @Suppress("SameParameterValue") + private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet { + val animatorSet = AnimatorSet() + + animatorSet.play( + createBackgroundAlphaAnimator(isStashed).apply { + val alphaDuration = if (isStashed) duration else TASKBAR_STASH_ALPHA_DURATION + val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L + this.duration = max(0L, alphaDuration - alphaDelay) + this.startDelay = alphaDelay + this.interpolator = LINEAR + } + ) + + animatorSet.play( + bubbleBarBubbleAlpha + .animateToValue(getBarAlphaStart(isStashed), getBarAlphaEnd(isStashed)) + .apply { + this.duration = TASKBAR_STASH_ALPHA_DURATION + this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY + this.interpolator = LINEAR + } + ) + + animatorSet.play( + createSpringOnStashAnimator(isStashed).apply { + this.duration = duration + this.interpolator = LINEAR + } + ) + + animatorSet.play( + bubbleBarViewController.createRevealAnimatorForStashChange(isStashed).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + // Animate bubble translation to keep reveal animation in the bounds of the bar + val bubbleTyStart = if (isStashed) 0f else -bubbleBarTranslationY + val bubbleTyEnd = if (isStashed) -bubbleBarTranslationY else 0f + animatorSet.play( + bubbleBarBubbleTranslationY.animateToValue(bubbleTyStart, bubbleTyEnd).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + animatorSet.play( + bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + val pivotX = if (bubbleBarViewController.isBubbleBarOnLeft) 0f else 1f + animatorSet.play( + createScaleAnimator(isStashed).apply { + this.duration = duration + this.interpolator = EMPHASIZED + this.setBubbleBarPivotDuringAnim(pivotX, 1f) + } + ) + + val translationYTarget = if (isStashed) getStashTranslation() else bubbleBarTranslationY + animatorSet.play( + bubbleBarTranslationYAnimator.animateToValue(translationYTarget).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + animatorSet.doOnStart { + // Update the start value for bubble view and background alpha when the entire animation + // begins. + // Alpha animation has a delay, and if we set the initial values at the start of the + // alpha animation, it will cause flickers. + bubbleBarBubbleAlpha.updateValue(getBarAlphaStart(isStashed)) + bubbleBarBackgroundAlpha.updateValue(getBarAlphaStart(isStashed)) + // We animate alpha for background and bubble views separately. Make sure the container + // is always visible. + bubbleBarAlpha.value = 1f + } + animatorSet.doOnEnd { + animator = null + controllersAfterInitAction.runAfterInit { + if (isStashed) { + bubbleBarAlpha.value = 0f + // reset bubble view alpha + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + // reset stash translation + translationYDuringStash.updateValue(0f) + bubbleBarBubbleTranslationY.updateValue(0f) + bubbleBarViewController.isExpanded = false + } + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + } + return animatorSet + } + + private fun createBackgroundAlphaAnimator(isStashed: Boolean): AnimatorSet { + return AnimatorSet().apply { + play( + bubbleBarBackgroundAlpha.animateToValue( + getBarAlphaStart(isStashed), + getBarAlphaEnd(isStashed), + ) + ) + play(stashHandleViewAlpha?.animateToValue(getHandleAlphaEnd(isStashed))) + } + } + + private fun getBarAlphaStart(isStashed: Boolean): Float { + return if (isStashed) 1f else 0f + } + + private fun getBarAlphaEnd(isStashed: Boolean): Float { + return if (isStashed) 0f else 1f + } + + private fun getHandleAlphaEnd(isStashed: Boolean): Float { + return if (isStashed) 1f else 0f + } + + private fun createSpringOnStashAnimator(isStashed: Boolean): Animator { + if (!isStashed) { + // Animate the stash translation back to 0 + return translationYDuringStash.animateToValue(0f) + } + // Apply a spring to the handle + return SpringAnimationBuilder(context) + .setStartValue(translationYDuringStash.value) + .setEndValue(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .setStartVelocity(stashHandleStashVelocity) + .build(translationYDuringStash, AnimatedFloat.VALUE) + } + + private fun createScaleAnimator(isStashed: Boolean): AnimatorSet { + val scaleXTarget = if (isStashed) getStashScaleX() else 1f + val scaleYTarget = if (isStashed) getStashScaleY() else 1f + return AnimatorSet().apply { + play(bubbleBarBackgroundScaleX.animateToValue(scaleXTarget)) + play(bubbleBarBackgroundScaleY.animateToValue(scaleYTarget)) + } + } + + private fun onIsStashedChanged() { + controllersAfterInitAction.runAfterInit { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + bubbleStashedHandleViewController?.onIsStashedChanged() + } + } + + private fun animateBubbleBarYToHotseat() { + translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForHotseat) + } + + private fun animateBubbleBarYToTaskbar() { + translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForTaskbar) + } + + private fun translateBubbleBarYUpdateTouchRegionOnCompletion(toY: Float) { + bubbleBarViewController.bubbleBarTranslationY + .animateToValue(toY) + .updateTouchRegionOnAnimationEnd() + .setDuration(BAR_TRANSLATION_DURATION) + .start() + } + + @VisibleForTesting + fun updateStashedAndExpandedState(stash: Boolean, expand: Boolean) { + if (bubbleBarViewController.isHiddenForNoBubbles) { + // If there are no bubbles the bar and handle are invisible, nothing to do here. + return + } + val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview + if (this.isStashed != isStashed) { + this.isStashed = isStashed + // notify the view controller that the stash state is about to change so that it can + // cancel an ongoing animation if there is one. + // note that this has to be called before updating mIsStashed with the new value, + // otherwise interrupting an ongoing animation may update it again with the wrong state + bubbleBarViewController.onStashStateChanging() + animator?.cancel() + animator = + createStashAnimator(isStashed, BAR_STASH_DURATION).apply { + updateTouchRegionOnAnimationEnd() + start() + } + } + if (bubbleBarViewController.isExpanded != expand) { + bubbleBarViewController.isExpanded = expand + } + } + + private fun Animator.updateTouchRegionOnAnimationEnd(): Animator { + doOnEnd { onIsStashedChanged() } + return this + } + + private fun Animator.setBubbleBarPivotDuringAnim(pivotX: Float, pivotY: Float): Animator { + var initialPivotX = Float.NaN + var initialPivotY = Float.NaN + doOnStart { + initialPivotX = bubbleBarViewController.bubbleBarRelativePivotX + initialPivotY = bubbleBarViewController.bubbleBarRelativePivotY + bubbleBarViewController.setBubbleBarRelativePivot(pivotX, pivotY) + } + doOnEnd { + if (!initialPivotX.isNaN() && !initialPivotY.isNaN()) { + bubbleBarViewController.setBubbleBarRelativePivot(initialPivotX, initialPivotY) + } + } + return this + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt new file mode 100644 index 0000000000..e384586b45 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt @@ -0,0 +1,40 @@ +/* + * 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.customization + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.launcher3.Insettable +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.views.ActivityContext + +/** TaskbarView that is customizeable via Taskbar containers. */ +class CustomizableTaskbarView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs), Insettable { + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + + init { + inflate(context, R.layout.customizable_taskbar_view, this) + } + + override fun setInsets(insets: Rect?) { + // Ignore, we just implement Insettable to draw behind system insets. + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt new file mode 100644 index 0000000000..e6c0b2f087 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt @@ -0,0 +1,145 @@ +/* + * 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.customization + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color.TRANSPARENT +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.core.view.setPadding +import com.android.launcher3.R +import com.android.launcher3.Utilities.dpToPx +import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarViewCallbacks +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.views.IconButtonView +import com.android.quickstep.DeviceConfigWrapper +import com.android.quickstep.util.AssistStateManager + +/** Taskbar all apps button container for customizable taskbar. */ +class TaskbarAllAppsButtonContainer +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + IconButtonView(context, attrs), TaskbarContainer { + + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + private var allAppsTouchTriggered = false + private var allAppsTouchRunnable: Runnable? = null + private var allAppsButtonTouchDelayMs: Long = ViewConfiguration.getLongPressTimeout().toLong() + private lateinit var taskbarViewCallbacks: TaskbarViewCallbacks + + override val spaceNeeded: Int + get() { + return dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat()) + } + + init { + LayoutInflater.from(context).inflate(R.layout.taskbar_all_apps_button, null, false) + setUpIcon() + } + + @SuppressLint("UseCompatLoadingForDrawables", "ResourceAsColor") + private fun setUpIcon() { + val drawable = + resources.getDrawable( + getAllAppsButton(activityContext.taskbarFeatureEvaluator.isTransient) + ) + backgroundTintList = ColorStateList.valueOf(TRANSPARENT) + setIconDrawable(drawable) + setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat())) + setForegroundTint(activityContext.getColor(R.color.all_apps_button_color)) + } + + @SuppressLint("ClickableViewAccessibility") + fun setUpCallbacks(callbacks: TaskbarViewCallbacks) { + taskbarViewCallbacks = callbacks + setOnClickListener(this::onAllAppsButtonClick) + setOnLongClickListener(this::onAllAppsButtonLongClick) + setOnTouchListener(this::onAllAppsButtonTouch) + isHapticFeedbackEnabled = taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled() + allAppsTouchRunnable = Runnable { + taskbarViewCallbacks.triggerAllAppsButtonLongClick() + allAppsTouchTriggered = true + } + val assistStateManager = AssistStateManager.INSTANCE[mContext] + if ( + DeviceConfigWrapper.get().customLpaaThresholds && + assistStateManager.lpnhDurationMillis.isPresent + ) { + allAppsButtonTouchDelayMs = assistStateManager.lpnhDurationMillis.get() + } + } + + @DrawableRes + private fun getAllAppsButton(isTransientTaskbar: Boolean): Int { + val shouldSelectTransientIcon = + isTransientTaskbar || (enableTaskbarPinning() && !activityContext.isThreeButtonNav) + return if (shouldSelectTransientIcon) R.drawable.ic_transient_taskbar_all_apps_search_button + else R.drawable.ic_taskbar_all_apps_search_button + } + + @DimenRes + fun getAllAppsButtonTranslationXOffset(isTransientTaskbar: Boolean): Int { + return if (isTransientTaskbar) { + R.dimen.transient_taskbar_all_apps_button_translation_x_offset + } else { + R.dimen.taskbar_all_apps_search_button_translation_x_offset + } + } + + private fun onAllAppsButtonTouch(view: View, ev: MotionEvent): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + allAppsTouchTriggered = false + MAIN_EXECUTOR.handler.postDelayed(allAppsTouchRunnable!!, allAppsButtonTouchDelayMs) + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> cancelAllAppsButtonTouch() + } + return false + } + + private fun cancelAllAppsButtonTouch() { + MAIN_EXECUTOR.handler.removeCallbacks(allAppsTouchRunnable!!) + // ACTION_UP is first triggered, then click listener / long-click listener is triggered on + // the next frame, so we need to post twice and delay the reset. + this.post { this.post { allAppsTouchTriggered = false } } + } + + private fun onAllAppsButtonClick(view: View) { + if (!allAppsTouchTriggered) { + taskbarViewCallbacks.triggerAllAppsButtonClick(view) + } + } + + // Handle long click from Switch Access and Voice Access + private fun onAllAppsButtonLongClick(view: View): Boolean { + if (!MAIN_EXECUTOR.handler.hasCallbacks(allAppsTouchRunnable!!) && !allAppsTouchTriggered) { + taskbarViewCallbacks.triggerAllAppsButtonLongClick() + } + return true + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt index 3c4b63a1d7..35ae43cd29 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.launcher3.taskbar.customization -/** Enums for all feature container that taskbar supports. */ -enum class TaskbarContainer { - ALL_APPS, - DIVIDER, - APP_ICONS, - RECENTS, - NAV_BUTTONS, - BUBBLES, +import androidx.annotation.Dimension + +/** + * Interface to be implemented by all taskbar container to expose [spaceNeeded] for each container. + */ +interface TaskbarContainer { + @get:Dimension(unit = Dimension.DP) val spaceNeeded: Int } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt new file mode 100644 index 0000000000..d4548f5efe --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt @@ -0,0 +1,27 @@ +/* + * 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.customization + +/** Enums for all feature container that taskbar supports. */ +enum class TaskbarContainers { + ALL_APPS, + DIVIDER, + APP_ICONS, + RECENTS, + NAV_BUTTONS, + BUBBLES, +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt new file mode 100644 index 0000000000..1fb835ab33 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt @@ -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.launcher3.taskbar.customization + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color.TRANSPARENT +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.core.view.setPadding +import com.android.launcher3.R +import com.android.launcher3.Utilities.dpToPx +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarViewCallbacks +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.views.IconButtonView + +/** Taskbar divider view container for customizable taskbar. */ +class TaskbarDividerContainer +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : IconButtonView(context, attrs), TaskbarContainer { + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + + override val spaceNeeded: Int + get() { + return dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat()) + } + + init { + LayoutInflater.from(context).inflate(R.layout.taskbar_divider, null, false) + setUpIcon() + } + + @SuppressLint("UseCompatLoadingForDrawables") + fun setUpIcon() { + backgroundTintList = ColorStateList.valueOf(TRANSPARENT) + val drawable = resources.getDrawable(R.drawable.taskbar_divider_button) + setIconDrawable(drawable) + setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat())) + } + + @SuppressLint("ClickableViewAccessibility") + fun setUpCallbacks(callbacks: TaskbarViewCallbacks) { + setOnLongClickListener(callbacks.taskbarDividerLongClickListener) + setOnTouchListener(callbacks.taskbarDividerRightClickListener) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt index 1ec075ffaa..7739a0e3df 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt @@ -16,29 +16,50 @@ package com.android.launcher3.taskbar.customization +import com.android.launcher3.Flags.enableRecentsInTaskbar import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning import com.android.launcher3.taskbar.TaskbarActivityContext -import com.android.launcher3.taskbar.TaskbarControllers -import com.android.launcher3.taskbar.TaskbarRecentAppsController import com.android.launcher3.util.DisplayController /** Evaluates all the features taskbar can have. */ -class TaskbarFeatureEvaluator( +class TaskbarFeatureEvaluator +private constructor( private val taskbarActivityContext: TaskbarActivityContext, - private val taskbarControllers: TaskbarControllers, ) { - val hasAllApps = true val hasAppIcons = true val hasBubbles = false val hasNavButtons = taskbarActivityContext.isThreeButtonNav - val hasRecents: Boolean - get() = taskbarControllers.taskbarRecentAppsController.isEnabled + val isRecentsEnabled: Boolean + get() = enableRecentsInTaskbar() val hasDivider: Boolean - get() = enableTaskbarPinning() || hasRecents + get() = enableTaskbarPinning() || isRecentsEnabled val isTransient: Boolean get() = DisplayController.isTransientTaskbar(taskbarActivityContext) + + val isLandscape: Boolean + get() = taskbarActivityContext.deviceProfile.isLandscape + + fun onDestroy() { + taskbarFeatureEvaluator = null + } + + companion object { + @Volatile private var taskbarFeatureEvaluator: TaskbarFeatureEvaluator? = null + + @JvmStatic + fun getInstance( + taskbarActivityContext: TaskbarActivityContext, + ): TaskbarFeatureEvaluator { + synchronized(this) { + if (taskbarFeatureEvaluator == null) { + taskbarFeatureEvaluator = TaskbarFeatureEvaluator(taskbarActivityContext) + } + return taskbarFeatureEvaluator!! + } + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt index 4cd895de4d..e55cb1f50d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt @@ -19,23 +19,35 @@ package com.android.launcher3.taskbar.customization /** Taskbar Icon Specs */ object TaskbarIconSpecs { - val iconSize40dp = TaskbarIconSize(40) - val iconSize44dp = TaskbarIconSize(44) - val iconSize48dp = TaskbarIconSize(48) - val iconSize52dp = TaskbarIconSize(52) + // Mapping of visual icon size to icon specs value http://b/235886078 + val iconSize40dp = TaskbarIconSize(44) + val iconSize44dp = TaskbarIconSize(48) + val iconSize48dp = TaskbarIconSize(52) + val iconSize52dp = TaskbarIconSize(57) val transientTaskbarIconSizes = arrayOf(iconSize44dp, iconSize48dp, iconSize52dp) val defaultPersistentIconSize = iconSize40dp val defaultTransientIconSize = iconSize44dp - // defined as row, columns + val minimumIconSize = iconSize40dp + + val defaultPersistentIconMargin = TaskbarIconMarginSize(6) + val defaultTransientIconMargin = TaskbarIconMarginSize(12) + + val minimumTaskbarIconTouchSize = TaskbarIconSize(48) + + val transientOrPinnedTaskbarIconPaddingSize = iconSize52dp + val transientTaskbarIconSizeByGridSize = mapOf( - Pair(6, 5) to iconSize52dp, - Pair(4, 5) to iconSize48dp, - Pair(5, 4) to iconSize48dp, - Pair(4, 4) to iconSize48dp, - Pair(5, 6) to iconSize44dp, + TransientTaskbarIconSizeKey(6, 5, false) to iconSize52dp, + TransientTaskbarIconSizeKey(6, 5, true) to iconSize52dp, + TransientTaskbarIconSizeKey(4, 4, false) to iconSize48dp, + TransientTaskbarIconSizeKey(4, 4, true) to iconSize52dp, + TransientTaskbarIconSizeKey(4, 5, false) to iconSize48dp, + TransientTaskbarIconSizeKey(4, 5, true) to iconSize48dp, + TransientTaskbarIconSizeKey(5, 5, false) to iconSize44dp, + TransientTaskbarIconSizeKey(5, 5, true) to iconSize44dp, ) } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt index 02e5947b32..822ca6460f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt @@ -16,13 +16,41 @@ package com.android.launcher3.taskbar.customization -/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */ -class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator) { +import com.android.launcher3.taskbar.TaskbarActivityContext - fun getIconSizeByGrid(row: Int, column: Int): TaskbarIconSize { +/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */ +class TaskbarSpecsEvaluator( + private val taskbarActivityContext: TaskbarActivityContext, + private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator, + numRows: Int = taskbarActivityContext.deviceProfile.inv.numRows, + numColumns: Int = taskbarActivityContext.deviceProfile.inv.numColumns, +) { + var taskbarIconSize: TaskbarIconSize = getIconSizeByGrid(numColumns, numRows) + + // TODO(b/341146605) : initialize it to taskbar container in later cl. + private var taskbarContainer: List = emptyList() + + val taskbarIconPadding: Int = + if ( + TaskbarIconSpecs.transientOrPinnedTaskbarIconPaddingSize.size > taskbarIconSize.size && + !taskbarFeatureEvaluator.hasNavButtons + ) { + (TaskbarIconSpecs.iconSize52dp.size - taskbarIconSize.size) / 2 + } else { + 0 + } + + val taskbarIconMargin: TaskbarIconMarginSize = + if (taskbarFeatureEvaluator.isTransient) { + TaskbarIconSpecs.defaultTransientIconMargin + } else { + TaskbarIconSpecs.defaultPersistentIconMargin + } + + fun getIconSizeByGrid(columns: Int, rows: Int): TaskbarIconSize { return if (taskbarFeatureEvaluator.isTransient) { TaskbarIconSpecs.transientTaskbarIconSizeByGridSize.getOrDefault( - Pair(row, column), + TransientTaskbarIconSizeKey(columns, rows, taskbarFeatureEvaluator.isLandscape), TaskbarIconSpecs.defaultTransientIconSize, ) } else { @@ -36,8 +64,11 @@ class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureE val currentIconSizeIndex = TaskbarIconSpecs.transientTaskbarIconSizes.indexOf(iconSize) // return the current icon size if supplied icon size is unknown or we have reached the // min icon size. - return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) iconSize - else TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1] + return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) { + iconSize + } else { + TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1] + } } fun getIconSizeStepUp(iconSize: TaskbarIconSize): TaskbarIconSize { @@ -52,9 +83,28 @@ class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureE ) { iconSize } else { - TaskbarIconSpecs.transientTaskbarIconSizes.get(currentIconSizeIndex + 1) + TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex + 1] } } + + // TODO(jagrutdesai) : Call this in init once the containers are ready. + private fun calculateTaskbarIconSize() { + while ( + taskbarIconSize != TaskbarIconSpecs.minimumIconSize && + taskbarActivityContext.transientTaskbarBounds.width() < + calculateSpaceNeeded(taskbarContainer) + ) { + taskbarIconSize = getIconSizeStepDown(taskbarIconSize) + } + } + + private fun calculateSpaceNeeded(containers: List): Int { + return containers.sumOf { it.spaceNeeded } + } } data class TaskbarIconSize(val size: Int) + +data class TransientTaskbarIconSizeKey(val columns: Int, val rows: Int, val isLandscape: Boolean) + +data class TaskbarIconMarginSize(val size: Int) diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index 110ca167aa..4590efefb2 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -20,6 +20,7 @@ import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.Keyframe; @@ -39,6 +40,7 @@ import android.graphics.drawable.Drawable; import android.os.Process; import android.util.AttributeSet; import android.util.FloatProperty; +import android.util.Property; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -71,12 +73,27 @@ import java.util.List; */ public class PredictedAppIcon extends DoubleShadowBubbleTextView { + private static final float RING_SCALE_START_VALUE = 0.75f; private static final int RING_SHADOW_COLOR = 0x99000000; private static final float RING_EFFECT_RATIO = 0.095f; private static final long ICON_CHANGE_ANIM_DURATION = 360; private static final long ICON_CHANGE_ANIM_STAGGER = 50; + private static final Property RING_SCALE_PROPERTY = + new Property<>(Float.TYPE, "ringScale") { + @Override + public Float get(PredictedAppIcon icon) { + return icon.mRingScale; + } + + @Override + public void set(PredictedAppIcon icon, Float value) { + icon.mRingScale = value; + icon.invalidate(); + } + }; + boolean mIsDrawingDot = false; private final DeviceProfile mDeviceProfile; private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -96,6 +113,11 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { private Animator mSlotMachineAnim; private float mSlotMachineIconTranslationY; + // Used to animate the "ring" around predicted icons + private float mRingScale = 1f; + private boolean mForceHideRing = false; + private Animator mRingScaleAnim; + private static final FloatProperty SLOT_MACHINE_TRANSLATION_Y = new FloatProperty("slotMachineTranslationY") { @Override @@ -356,17 +378,52 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { } } + @Override + public void setForceHideRing(boolean forceHideRing) { + if (mForceHideRing == forceHideRing) { + return; + } + mForceHideRing = forceHideRing; + + if (forceHideRing) { + invalidate(); + } else { + animateRingScale(RING_SCALE_START_VALUE, 1); + } + } + + private void cancelRingScaleAnim() { + if (mRingScaleAnim != null) { + mRingScaleAnim.cancel(); + } + } + + private void animateRingScale(float... ringScale) { + cancelRingScaleAnim(); + mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale); + mRingScaleAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRingScaleAnim = null; + } + }); + mRingScaleAnim.start(); + } + private void drawEffect(Canvas canvas) { - // Don't draw ring effect if item is about to be dragged. - if (mDrawForDrag) { + // Don't draw ring effect if item is about to be dragged or if the icon is not visible. + if (mDrawForDrag || !mIsIconVisible || mForceHideRing) { return; } mIconRingPaint.setColor(RING_SHADOW_COLOR); mIconRingPaint.setMaskFilter(mShadowFilter); + int count = canvas.save(); + canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f); canvas.drawPath(mRingPath, mIconRingPaint); mIconRingPaint.setColor(mPlateColor); mIconRingPaint.setMaskFilter(null); canvas.drawPath(mRingPath, mIconRingPaint); + canvas.restoreToCount(count); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java index 039c0a0ad8..26a1322ea6 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java @@ -22,7 +22,6 @@ import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.Intent; -import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.Pair; @@ -30,7 +29,6 @@ import android.view.View; import android.widget.RemoteViews; import android.window.SplashScreen; -import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ActivityOptionsWrapper; @@ -66,14 +64,8 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { } Pair options = remoteResponse.getLaunchOptions(view); ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager() - .getActivityLaunchOptions(hostView); - Object itemInfo = hostView.getTag(); - IBinder launchCookie = null; - if (itemInfo instanceof ItemInfo) { - launchCookie = mLauncher.getLaunchCookie((ItemInfo) itemInfo); - activityOptions.options.setLaunchCookie(launchCookie); - } - if (Utilities.ATLEAST_S && !pendingIntent.isActivity()) { + .getActivityLaunchOptions(hostView, (ItemInfo) hostView.getTag()); + if (!pendingIntent.isActivity()) { // In the event this pending intent eventually launches an activity, i.e. a trampoline, // use the Quickstep transition animation. try { @@ -81,7 +73,7 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { .registerRemoteAnimationForNextActivityStart( pendingIntent.getCreatorPackage(), activityOptions.options.getRemoteAnimationAdapter(), - launchCookie); + activityOptions.options.getLaunchCookie()); } catch (RemoteException e) { // Do nothing. } @@ -92,7 +84,7 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); options = Pair.create(options.first, activityOptions.options); if (pendingIntent.isActivity()) { - logAppLaunch(itemInfo); + logAppLaunch(hostView.getTag()); } return RemoteViews.startPendingIntent(hostView, pendingIntent, options); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 2168f7a318..e80e838e90 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -19,10 +19,10 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.os.Trace.TRACE_TAG_APP; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; -import static com.android.launcher3.Flags.enablePredictiveBackGesture; import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE; @@ -42,9 +42,9 @@ import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED; -import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition; import static com.android.launcher3.popup.SystemShortcut.APP_INFO; +import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT; import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP; import static com.android.launcher3.popup.SystemShortcut.INSTALL; import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL; @@ -60,12 +60,12 @@ import static com.android.launcher3.testing.shared.TestProtocol.QUICK_SWITCH_STA import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN; import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +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.AnimUtils.completeRunnableListCallback; import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; -import static com.android.window.flags.Flags.enableDesktopWindowingMode; -import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -74,6 +74,7 @@ import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.graphics.RectF; @@ -81,7 +82,6 @@ import android.hardware.display.DisplayManager; import android.media.permission.SafeCloseable; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; import android.os.IRemoteCallback; import android.os.SystemProperties; import android.os.Trace; @@ -172,6 +172,7 @@ import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TouchInteractionService.TISBinder; +import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.AsyncClockEventDelegate; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LauncherUnfoldAnimationController; @@ -197,6 +198,7 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig; import com.android.systemui.unfold.dagger.UnfoldMain; import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver; import com.android.systemui.unfold.updates.RotationChangeProvider; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import kotlin.Unit; @@ -212,7 +214,8 @@ import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Stream; -public class QuickstepLauncher extends Launcher implements RecentsViewContainer { +public class QuickstepLauncher extends Launcher implements RecentsViewContainer, + SystemShortcut.BubbleActivityStarter { private static final boolean TRACE_LAYOUTS = SystemProperties.getBoolean("persist.debug.trace_layouts", false); private static final String TRACE_RELAYOUT_CLASS = @@ -224,7 +227,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer private FixedContainerItems mAllAppsPredictions; private HotseatPredictionController mHotseatPredictionController; private DepthController mDepthController; - private @Nullable DesktopVisibilityController mDesktopVisibilityController; private QuickstepTransitionManager mAppTransitionManager; private OverviewActionsView mActionsView; @@ -253,6 +255,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer private boolean mIsPredictiveBackToHomeInProgress; + private boolean mCanShowAllAppsEducationView; + + private boolean mIsOverlayVisible; + public static QuickstepLauncher getLauncher(Context context) { return fromContext(context); } @@ -273,7 +279,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver( asContext(), deviceState); - if (enableDesktopWindowingMode()) { + if (DesktopModeStatus.canEnterDesktopMode(this)) { mDesktopRecentsTransitionController = new DesktopRecentsTransitionController( getStateManager(), systemUiProxy, getIApplicationThread(), getDepthController()); @@ -293,9 +299,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mTISBindHelper = new TISBindHelper(this, this::onTISConnected); mDepthController = new DepthController(this); - if (enableDesktopWindowingMode()) { - mDesktopVisibilityController = new DesktopVisibilityController(this); - mDesktopVisibilityController.registerSystemUiListener(); + if (DesktopModeStatus.canEnterDesktopMode(this)) { mSplitSelectStateController.initSplitFromDesktopController(this, overviewComponentObserver); } @@ -459,6 +463,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer if (Flags.enablePrivateSpace()) { shortcuts.add(UNINSTALL_APP); } + if (com.android.wm.shell.Flags.enableBubbleAnything()) { + shortcuts.add(BUBBLE_SHORTCUT); + } return shortcuts.stream(); } @@ -486,11 +493,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0; if (started) { DeviceProfile profile = getDeviceProfile(); - boolean willUserBeActive = - (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0; boolean visible = (state == NORMAL || state == OVERVIEW) - && (willUserBeActive || isUserActive()) - && !profile.isVerticalBarLayout(); + && isUserActive() + && !profile.isVerticalBarLayout() + && !mIsOverlayVisible; SystemUiProxy.INSTANCE.get(this) .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx); } @@ -499,6 +505,12 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } } + @Override + public void onOverlayVisibilityChanged(boolean visible) { + super.onOverlayVisibilityChanged(visible); + mIsOverlayVisible = visible; + } + @Override public void bindExtraContainerItems(FixedContainerItems item) { if (item.containerId == Favorites.CONTAINER_PREDICTION) { @@ -510,7 +522,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { mHotseatPredictionController.setPredictedItems(item); } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) { - getPopupDataProvider().setRecommendedWidgets(item.items); + getWidgetPickerDataProvider().setWidgetRecommendations(item.items); } } @@ -539,10 +551,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mLauncherUnfoldAnimationController.onDestroy(); } - if (mDesktopVisibilityController != null) { - mDesktopVisibilityController.unregisterSystemUiListener(); - } - if (mSplitSelectStateController != null) { mSplitSelectStateController.onDestroy(); } @@ -581,9 +589,19 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } case QUICK_SWITCH_STATE_ORDINAL: { RecentsView rv = getOverviewPanel(); - TaskView tasktolaunch = rv.getCurrentPageTaskView(); - if (tasktolaunch != null) { - tasktolaunch.launchTask(success -> { + TaskView currentPageTask = rv.getCurrentPageTaskView(); + TaskView fallbackTask = rv.getTaskViewAt(0); + if (currentPageTask != null || fallbackTask != null) { + TaskView taskToLaunch = currentPageTask; + if (currentPageTask == null) { + taskToLaunch = fallbackTask; + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home fallback case: The TaskView at index ") + .append(rv.getCurrentPage()) + .append(" is missing."), + QUICK_SWITCH_FROM_HOME_FALLBACK); + } + taskToLaunch.launchWithoutAnimation(success -> { if (!success) { getStateManager().goToState(OVERVIEW); } else { @@ -592,6 +610,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return Unit.INSTANCE; }); } else { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home failed: TaskViews at indices ") + .append(rv.getCurrentPage()) + .append(" and 0 are missing."), + QUICK_SWITCH_FROM_HOME_FAILED); getStateManager().goToState(NORMAL); } break; @@ -661,9 +684,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to // work, we must opt-in BEFORE registering back dispatcher. So we need to call // setEnableOnBackInvokedCallback() before super.onCreate() - if (Utilities.ATLEAST_U && enablePredictiveBackGesture()) { - getApplicationInfo().setEnableOnBackInvokedCallback(true); - } + getApplicationInfo().setEnableOnBackInvokedCallback(true); super.onCreate(savedInstanceState); if (savedInstanceState != null) { mPendingSplitSelectInfo = ObjectWrapper.unwrap( @@ -671,9 +692,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } addMultiWindowModeChangedListener(mDepthController); initUnfoldTransitionProgressProvider(); - if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { - mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow()); - } + mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow()); getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE); QuickstepOnboardingPrefs.setup(this); View.setTraceLayoutSteps(TRACE_LAYOUTS); @@ -694,7 +713,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // Check if there is already an instance of this app running, if so, initiate the split // using that. mSplitSelectStateController.findLastActiveTasksAndRunCallback( - Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()), + Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()), false /* findExactPairMatch */, foundTasks -> { @Nullable Task foundTask = foundTasks[0]; @@ -721,7 +740,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer Rect tempRect = new Rect(); mSplitSelectStateController.setInitialTaskSelect(source.intent, - source.position.stagePosition, source.itemInfo, source.splitEvent, + source.position.stagePosition, source.getItemInfo(), source.splitEvent, source.alreadyRunningTaskId); RecentsView recentsView = getOverviewPanel(); @@ -739,6 +758,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer floatingTaskView.setOnClickListener(view -> mSplitSelectStateController.getSplitAnimationController(). playAnimPlaceholderToFullscreen(this, view, Optional.empty())); + floatingTaskView.setContentDescription(source.getItemInfo().contentDescription); + mSplitSelectStateController.setFirstFloatingTaskView(floatingTaskView); anim.addListener(new AnimatorListenerAdapter() { @Override @@ -873,8 +894,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // event won't go through ViewRootImpl#InputStage#onProcess. // So when receive back key, try to do the same check thing in // ViewRootImpl#NativePreImeInputStage#onProcess - if (!Utilities.ATLEAST_U || !enablePredictiveBackGesture() - || event.getKeyCode() != KeyEvent.KEYCODE_BACK + if (event.getKeyCode() != KeyEvent.KEYCODE_BACK || event.getAction() != KeyEvent.ACTION_UP || event.isCanceled()) { return false; } @@ -885,10 +905,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override protected void registerBackDispatcher() { - if (!enablePredictiveBackGesture()) { - super.registerBackDispatcher(); - return; - } getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, new OnBackAnimationCallback() { @@ -986,10 +1002,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public void setResumed() { - if (!enableDesktopWindowingWallpaperActivity() - && mDesktopVisibilityController != null - && mDesktopVisibilityController.areDesktopTasksVisible() - && !mDesktopVisibilityController.isRecentsGestureInProgress()) { + DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController(); + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + && desktopVisibilityController != null + && desktopVisibilityController.areDesktopTasksVisible() + && !desktopVisibilityController.isRecentsGestureInProgress()) { // Return early to skip setting activity to appear as resumed // TODO: b/333533253 - Remove after flag rollout return; @@ -1129,8 +1146,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } @Nullable + @Override public DesktopVisibilityController getDesktopVisibilityController() { - return mDesktopVisibilityController; + return mTISBindHelper.getDesktopVisibilityController(); } @Nullable @@ -1165,7 +1183,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { - ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(v); + ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions( + v, item != null ? item : (ItemInfo) v.getTag()); if (mLastTouchUpTime > 0) { activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER, mLastTouchUpTime); @@ -1182,7 +1201,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer : Display.DEFAULT_DISPLAY); activityOptions.options.setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - addLaunchCookie(item, activityOptions.options); return activityOptions; } @@ -1206,60 +1224,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop); } - /** - * Adds a new launch cookie for the activity launch if supported. - * - * @param info the item info for the launch - * @param opts the options to set the launchCookie on. - */ - public void addLaunchCookie(ItemInfo info, ActivityOptions opts) { - IBinder launchCookie = getLaunchCookie(info); - if (launchCookie != null) { - opts.setLaunchCookie(launchCookie); - } - } - - /** - * Return a new launch cookie for the activity launch if supported. - * - * @param info the item info for the launch - */ - public IBinder getLaunchCookie(ItemInfo info) { - if (info == null) { - return null; - } - switch (info.container) { - case Favorites.CONTAINER_DESKTOP: - case Favorites.CONTAINER_HOTSEAT: - case Favorites.CONTAINER_PRIVATESPACE: - // Fall through and continue it's on the workspace (we don't support swiping back - // to other containers like all apps or the hotseat predictions (which can change) - break; - default: - if (info.container >= 0) { - // Also allow swiping to folders - break; - } - // Reset any existing launch cookies associated with the cookie - return ObjectWrapper.wrap(NO_MATCHING_ID); - } - switch (info.itemType) { - case Favorites.ITEM_TYPE_APPLICATION: - case Favorites.ITEM_TYPE_DEEP_SHORTCUT: - case Favorites.ITEM_TYPE_APPWIDGET: - // Fall through and continue if it's an app, shortcut, or widget - break; - default: - // Reset any existing launch cookies associated with the cookie - return ObjectWrapper.wrap(NO_MATCHING_ID); - } - return ObjectWrapper.wrap(new Integer(info.id)); - } - - public void setHintUserWillBeActive() { - addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE); - } - @Override public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) { super.onDisplayInfoChanged(context, info, flags); @@ -1338,8 +1302,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public boolean areDesktopTasksVisible() { - if (mDesktopVisibilityController != null) { - return mDesktopVisibilityController.areDesktopTasksVisible(); + DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController(); + if (desktopVisibilityController != null) { + return desktopVisibilityController.areDesktopTasksVisible(); } return false; } @@ -1367,10 +1332,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer */ public void launchSplitTasks( @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { - // Top/left and bottom/right tasks respectively. - Task task1 = groupTask.task1; + // SplitBounds can be null if coming from Taskbar launch. + final boolean firstTaskIsLeftTopTask = isFirstTaskLeftTopTask(groupTask); // task2 should never be null when calling this method. Allow a crash to catch invalid calls - Task task2 = groupTask.task2; + Task task1 = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; + Task task2 = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; mSplitSelectStateController.launchExistingSplitPair( null /* launchingTaskView */, task1.key.id, @@ -1384,12 +1350,23 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer remoteTransition); } + private static boolean isFirstTaskLeftTopTask(@NonNull GroupTask groupTask) { + return groupTask.mSplitBounds == null + || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; + } + /** * Launches two apps as an app pair. */ public void launchAppPair(AppPairIcon appPairIcon) { + // Potentially show the Taskbar education once the app pair launch finishes mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon, - CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE); + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE, + (success) -> { + if (success && mTaskbarUIController != null) { + mTaskbarUIController.showEduOnAppLaunch(); + } + }); } public boolean canStartHomeSafely() { @@ -1421,6 +1398,18 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return true; } + @Override + public void showShortcutBubble(ShortcutInfo info) { + if (info == null) return; + SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info); + } + + @Override + public void showAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) return; + SystemUiProxy.INSTANCE.get(this).showAppBubble(intent); + } + private static final class LauncherTaskViewController extends TaskViewTouchController { @@ -1490,4 +1479,12 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer public boolean isRecentsViewVisible() { return getStateManager().getState().isRecentsViewVisible; } + + public boolean isCanShowAllAppsEducationView() { + return mCanShowAllAppsEducationView; + } + + public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) { + mCanShowAllAppsEducationView = canShowAllAppsEducationView; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java index 01d5ff0e15..56fc4d1c0b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java @@ -17,7 +17,6 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; @@ -100,7 +99,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder { // for concurrent modification. new ArrayList<>(h.mProviderChangedListeners).forEach( ProviderChangedListener::notifyWidgetProvidersChanged))), - UI_HELPER_EXECUTOR.getLooper()); + getWidgetHolderExecutor().getLooper()); if (WIDGETS_ENABLED) { sWidgetHost.startListening(); } @@ -199,8 +198,10 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder { return; } - sWidgetHost.setAppWidgetHidden(); - setListeningFlag(false); + getWidgetHolderExecutor().execute(() -> { + sWidgetHost.setAppWidgetHidden(); + setListeningFlag(false); + }); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt index dc6365bc20..181cba098f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt @@ -79,6 +79,7 @@ import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP +import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN import com.android.launcher3.util.PluginManagerWrapper import com.android.launcher3.util.StartActivityParams import com.android.launcher3.util.UserIconInfo @@ -394,6 +395,7 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey ) addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey) + addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey) addOnboardPref("All Apps Visited Count", ALL_APPS_VISITED_COUNT.sharedPrefKey) } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java index fa80dc2776..030a7acb48 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -26,7 +26,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.quickstep.util.BaseDepthController; @@ -201,17 +200,6 @@ public class AllAppsState extends LauncherState { : previousState == OVERVIEW ? OVERVIEW : NORMAL; } - @Override - public float[] getOverviewScaleAndOffset(Launcher launcher) { - if (!FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) { - return super.getOverviewScaleAndOffset(launcher); - } - // This handles the case of returning to the previous app from Overview -> All Apps gesture. - // This is the start scale/offset of overview that will be used for that transition. - // TODO (b/283336332): Translate in Y direction (ideally with overview resistance). - return new float[] {0.5f /* scale */, NO_OFFSET}; - } - @Override public int getWorkspaceScrimColor(Launcher launcher) { return launcher.getDeviceProfile().isTablet diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java index 262564613a..18d717f096 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java @@ -17,7 +17,6 @@ package com.android.launcher3.uioverrides.states; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import android.content.Context; import android.graphics.Color; @@ -51,9 +50,11 @@ public class BackgroundAppState extends OverviewState { return super.getVerticalProgress(launcher); } RecentsView recentsView = launcher.getOverviewPanel(); - int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher, + int transitionLength = LayoutUtils.getShelfTrackingDistance( + launcher, launcher.getDeviceProfile(), - recentsView.getPagedOrientationHandler()); + recentsView.getPagedOrientationHandler(), + recentsView.getSizeStrategy()); AllAppsTransitionController controller = launcher.getAllAppsController(); float scrollRange = Math.max(controller.getShiftRange(), 1); float progressDelta = (transitionLength / scrollRange); @@ -107,8 +108,7 @@ public class BackgroundAppState extends OverviewState { @Override public boolean isTaskbarAlignedWithHotseat(Launcher launcher) { - if (ENABLE_SHELL_TRANSITIONS) return false; - return super.isTaskbarAlignedWithHotseat(launcher); + return false; } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java index 6822f1b39c..b165cddfb2 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java @@ -209,7 +209,7 @@ public class OverviewState extends LauncherState { TaskView taskView = recentsView.getRunningTaskView(); if (taskView != null) { if (recentsView.isTaskViewFullyVisible(taskView)) { - taskView.launchTasks(); + taskView.launchWithAnimation(); } else { recentsView.snapToPage(recentsView.indexOfChild(taskView)); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java index 0368f3a86d..3a39cf28b1 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java @@ -110,6 +110,7 @@ public class QuickstepAtomicAnimationFactory extends // taskbar icons disappearing before hotseat icons show up. float scrimUpperBoundFromSplit = QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration; + scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f); config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f)); config.setInterpolator(ANIM_SCRIM_FADE, fromState == OVERVIEW_SPLIT_SELECT diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java index 11e0ed528c..1d9e49201d 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java @@ -45,6 +45,7 @@ import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.DisplayController; @@ -53,6 +54,7 @@ import com.android.quickstep.TaskUtils; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.OverviewToHomeAnim; import com.android.quickstep.views.RecentsView; +import com.android.systemui.contextualeducation.GestureType; import java.util.function.BiConsumer; @@ -219,6 +221,8 @@ public class NavBarToHomeTouchController implements TouchController, } if (mStartState != mEndState) { logHomeGesture(); + ContextualEduStatsManager.INSTANCE.get(mLauncher).updateEduStats( + mSwipeDetector.isTrackpadGesture(), GestureType.HOME); } AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher); if (topOpenView != null) { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index 3325009b71..ff726e65d4 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -39,11 +39,9 @@ import android.view.MotionEvent; import android.view.ViewConfiguration; import com.android.internal.jank.Cuj; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -88,13 +86,17 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator. private ObjectAnimator mNormalToHintOverviewScrimAnimator; + private final QuickstepLauncher mLauncher; + private boolean mIsTrackpadSwipe; + /** * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled. * Animation should be added to the provided AnimatorSet */ - public NoButtonNavbarToOverviewTouchController(Launcher l, + public NoButtonNavbarToOverviewTouchController(QuickstepLauncher l, BiConsumer cancelSplitRunnable) { super(l); + mLauncher = l; mRecentsView = l.getOverviewPanel(); mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); @@ -104,7 +106,9 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch @Override protected boolean canInterceptTouch(MotionEvent ev) { - if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher) + mIsTrackpadSwipe = isTrackpadMotionEvent(ev); + mLauncher.setCanShowAllAppsEducationView(!mIsTrackpadSwipe); + if (!mIsTrackpadSwipe && DisplayController.getNavigationMode(mLauncher) == THREE_BUTTONS) { return false; } @@ -148,6 +152,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch super.onDragStart(start, startDisplacement); mMotionPauseDetector.clear(); + mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe); if (handlingOverviewAnim()) { InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, @@ -191,6 +196,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch } mMotionPauseDetector.clear(); + mIsTrackpadSwipe = false; mNormalToHintOverviewScrimAnimator = null; if (mLauncher.isInState(OVERVIEW)) { // Normally we would cleanup the state based on mCurrentAnimation, but since we stop @@ -214,11 +220,6 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mCancelSplitRunnable.accept(animatorSet, duration); animatorSet.start(); } - if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && - ((mFromState == NORMAL && mToState == ALL_APPS) - || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) { - mVibratorWrapper.vibrateForDragBump(); - } } private void onMotionPauseDetected() { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index ab277b60d3..9164405f1a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -121,6 +121,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, private AnimatorPlaybackController mNonOverviewAnim; private AnimatorPlaybackController mXOverviewAnim; private AnimatedFloat mYOverviewAnim; + private boolean mIsTrackpadSwipe; public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) { mLauncher = launcher; @@ -128,7 +129,10 @@ public class NoButtonQuickSwitchTouchController implements TouchController, mRecentsView = mLauncher.getOverviewPanel(); mXRange = mLauncher.getDeviceProfile().widthPx / 2f; mYRange = LayoutUtils.getShelfTrackingDistance( - mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler()); + mLauncher, + mLauncher.getDeviceProfile(), + mRecentsView.getPagedOrientationHandler(), + mRecentsView.getSizeStrategy()); mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange; mMotionPauseDetector = new MotionPauseDetector(mLauncher); mMotionPauseMinDisplacement = mLauncher.getResources().getDimension( @@ -177,7 +181,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, return false; } if (isTrackpadMultiFingerSwipe(ev)) { - return isTrackpadFourFingerSwipe(ev); + mIsTrackpadSwipe = isTrackpadFourFingerSwipe(ev); + return mIsTrackpadSwipe; } return true; } @@ -185,6 +190,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, @Override public void onDragStart(boolean start) { mMotionPauseDetector.clear(); + mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe); if (start) { InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH); InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java index b5914a1880..b562838091 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java @@ -29,7 +29,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.allapps.AllAppsTransitionController; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.AllAppsSwipeController; @@ -93,9 +92,7 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr @Override protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { if (fromState == ALL_APPS && !isDragTowardPositive) { - return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get() - ? mLauncher.getStateManager().getLastState() - : NORMAL; + return NORMAL; } else if (fromState == NORMAL && shouldOpenAllApps(isDragTowardPositive)) { return ALL_APPS; } @@ -145,8 +142,11 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr .createPlaybackController(); mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); RecentsView recentsView = mLauncher.getOverviewPanel(); - totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher, - mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler()); + totalShift = LayoutUtils.getShelfTrackingDistance( + mLauncher, + mLauncher.getDeviceProfile(), + recentsView.getPagedOrientationHandler(), + recentsView.getSizeStrategy()); } else { mCurrentAnimation = mLauncher.getStateManager() .createAnimationToNewWorkspace(mToState, config); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java index 93e4fbdc00..31e4e33d89 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java @@ -151,7 +151,7 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll int sysuiFlags = 0; TaskView tv = mOverviewPanel.getTaskViewAt(0); if (tv != null) { - sysuiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags(); + sysuiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags(); } mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags); } else { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java index d98e608b30..cb2c324e5d 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java @@ -58,8 +58,6 @@ public class StatusBarTouchController implements TouchController { /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/ private boolean mCanIntercept; - private boolean mIsTrackpadReverseScroll; - public StatusBarTouchController(Launcher l) { mLauncher = l; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher); @@ -95,8 +93,6 @@ public class StatusBarTouchController implements TouchController { } mDownEvents.clear(); mDownEvents.put(pid, new PointF(ev.getX(), ev.getY())); - mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled() - && isTrackpadScroll(ev); } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { // Check!! should only set it only when threshold is not entered. mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx))); @@ -107,9 +103,6 @@ public class StatusBarTouchController implements TouchController { if (action == ACTION_MOVE && mDownEvents.contains(pid)) { float dy = ev.getY(idx) - mDownEvents.get(pid).y; float dx = ev.getX(idx) - mDownEvents.get(pid).x; - if (mIsTrackpadReverseScroll) { - dy = -dy; - } // Currently input dispatcher will not do touch transfer if there are more than // one touch pointer. Hence, even if slope passed, only set the slippery flag // when there is single touch event. (context: InputDispatcher.cpp line 1445) @@ -134,7 +127,6 @@ public class StatusBarTouchController implements TouchController { mLauncher.getStatsLogManager().logger() .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN); setWindowSlippery(false); - mIsTrackpadReverseScroll = false; return true; } return true; @@ -161,9 +153,9 @@ public class StatusBarTouchController implements TouchController { } private boolean canInterceptTouch(MotionEvent ev) { - if (!mLauncher.isInState(LauncherState.NORMAL) || - AbstractFloatingView.getTopOpenViewWithType(mLauncher, - AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) { + if (isTrackpadScroll(ev) || !mLauncher.isInState(LauncherState.NORMAL) + || AbstractFloatingView.getTopOpenViewWithType(mLauncher, + AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) { return false; } else { // For NORMAL state, only listen if the event originated above the navbar height diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 4bc3c1661e..202276e1e9 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -22,9 +22,9 @@ import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; +import android.graphics.Rect; import android.os.VibrationEffect; import android.view.MotionEvent; -import android.view.View; import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; @@ -67,7 +67,7 @@ public abstract class TaskViewTouchController> - extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener, - RecentsAnimationCallbacks.RecentsAnimationListener { +public abstract class AbsSwipeUpHandler< + RECENTS_CONTAINER extends Context & RecentsViewContainer, + RECENTS_VIEW extends RecentsView, + STATE extends BaseState> + extends SwipeUpAnimationLogic + implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener { private static final String TAG = "AbsSwipeUpHandler"; private static final ArrayList STATE_NAMES = new ArrayList<>(); @@ -177,7 +183,7 @@ public abstract class AbsSwipeUpHandler mContainerInterface; + protected final BaseContainerInterface mContainerInterface; protected final InputConsumerProxy mInputConsumerProxy; protected final ActivityInitListener mActivityInitListener; // Callbacks to be made once the recents animation starts @@ -186,10 +192,9 @@ public abstract class AbsSwipeUpHandler snapshots = mGestureState.consumeRecentsAnimationCanceledSnapshot(); if (snapshots != null) { - mRecentsView.switchToScreenshot(snapshots, () -> { - if (mRecentsAnimationController != null) { - mRecentsAnimationController.cleanupScreenshot(); - } else if (mDeferredCleanupRecentsAnimationController != null) { - mDeferredCleanupRecentsAnimationController.cleanupScreenshot(); - mDeferredCleanupRecentsAnimationController = null; - } - }); + mRecentsView.switchToScreenshot(snapshots, () -> {}); mRecentsView.onRecentsAnimationComplete(); } }); @@ -558,7 +552,7 @@ public abstract class AbsSwipeUpHandler - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(isInAllAppsRegion)); - } - - private void buildAnimationController() { if (!canCreateNewOrUpdateExistingLauncherTransitionController()) { return; @@ -898,8 +873,6 @@ public abstract class AbsSwipeUpHandler= threshold); updateSysUiFlags(mCurrentShift.value); applyScrollAndTransform(); @@ -926,14 +899,13 @@ public abstract class AbsSwipeUpHandler 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; boolean quickswitchThresholdPassed = centermostTask != runningTask; // We will handle the sysui flags based on the centermost task view. mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed || (quickswitchThresholdPassed && centermostTaskFlags != 0)); - mRecentsAnimationController.setSplitScreenMinimized(mContext, swipeUpThresholdPassed); // Provide a hint to WM the direction that we will be settling in case the animation // needs to be canceled mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed); @@ -952,7 +924,7 @@ public abstract class AbsSwipeUpHandler taskTargetOptional = - Arrays.stream(appearedTaskTargets) - .filter(mGestureState.mLastStartedTaskIdPredicate) - .findFirst(); - if (!taskTargetOptional.isPresent()) { - ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets) + .filter(mGestureState.mLastStartedTaskIdPredicate) + .toArray(RemoteAnimationTarget[]::new); + if (taskTargets.length == 0) { + ActiveGestureLog.INSTANCE.addLog( + forceFinishReason.append("No appeared task matching started task id")); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - RemoteAnimationTarget taskTarget = taskTargetOptional.get(); + RemoteAnimationTarget taskTarget = taskTargets[0]; TaskView taskView = mRecentsView == null ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId); - if (taskView == null - || !taskView.getFirstThumbnailViewDeprecated().shouldShowSplashView()) { - ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + if (taskView == null || taskView.getTaskContainers().stream().noneMatch( + TaskContainer::getShouldShowSplashView)) { + ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed")); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } if (mContainer == null) { - ActiveGestureLog.INSTANCE.addLog("Activity destroyed"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed")); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash); + animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets); } private void animateSplashScreenExit( - @NonNull T activity, + @NonNull RECENTS_CONTAINER activity, @NonNull RemoteAnimationTarget[] appearedTaskTargets, - @NonNull SurfaceControl leash) { + @NonNull RemoteAnimationTarget[] animatingTargets) { ViewGroup splashView = activity.getDragLayer(); final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher ? (QuickstepLauncher) activity : null; @@ -2471,26 +2457,28 @@ public abstract class AbsSwipeUpHandler splashView.setAlpha(1)); } - finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1)); - } - }); + }); + } } private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) { diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 00cd60b1d6..87038439bf 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -21,17 +21,18 @@ import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe; import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION; import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; +import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_ATTACHED_ALPHA_ANIM; import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM; import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; +import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.graphics.Color; import android.view.MotionEvent; import androidx.annotation.Nullable; @@ -133,7 +134,7 @@ public abstract class BaseActivityInterface

@@ -358,6 +412,12 @@ public class TouchInteractionService extends Service { taskbarManager.onSystemBarAttributesChanged(displayId, behavior)); } + @Override + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.onTransitionModeUpdated(barMode, checkBarModes)); + } + @Override public void onNavButtonsDarkIntensityChanged(float darkIntensity) { executeForTaskbarManager(taskbarManager -> @@ -398,6 +458,18 @@ public class TouchInteractionService extends Service { return tis.mTaskbarManager; } + /** + * Returns the {@link DesktopVisibilityController} + *

+ * Returns {@code null} if TouchInteractionService is not connected + */ + @Nullable + public DesktopVisibilityController getDesktopVisibilityController() { + TouchInteractionService tis = mTis.get(); + if (tis == null) return null; + return tis.mDesktopVisibilityController; + } + @VisibleForTesting public void injectFakeTrackpadForTesting() { TouchInteractionService tis = mTis.get(); @@ -540,12 +612,12 @@ public class TouchInteractionService extends Service { private final TaskbarNavButtonCallbacks mNavCallbacks = new TaskbarNavButtonCallbacks() { @Override public void onNavigateHome() { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME); + mOverviewCommandHelper.addCommand(CommandType.HOME); } @Override public void onToggleOverview() { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE); + mOverviewCommandHelper.addCommand(CommandType.TOGGLE); } }; @@ -571,6 +643,10 @@ public class TouchInteractionService extends Service { private InputManager mInputManager; private final Set mTrackpadsConnected = new ArraySet<>(); + private NavigationMode mGestureStartNavMode = null; + + private DesktopVisibilityController mDesktopVisibilityController; + @Override public void onCreate() { super.onCreate(); @@ -583,15 +659,15 @@ public class TouchInteractionService extends Service { mAllAppsActionManager = new AllAppsActionManager( this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent); mInputManager = getSystemService(InputManager.class); - if (ENABLE_TRACKPAD_GESTURE.get()) { - mInputManager.registerInputDeviceListener(mInputDeviceListener, - UI_HELPER_EXECUTOR.getHandler()); - int [] inputDevices = mInputManager.getInputDeviceIds(); - for (int inputDeviceId : inputDevices) { - mInputDeviceListener.onInputDeviceAdded(inputDeviceId); - } + mInputManager.registerInputDeviceListener(mInputDeviceListener, + UI_HELPER_EXECUTOR.getHandler()); + int [] inputDevices = mInputManager.getInputDeviceIds(); + for (int inputDeviceId : inputDevices) { + mInputDeviceListener.onInputDeviceAdded(inputDeviceId); } - mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks); + mDesktopVisibilityController = new DesktopVisibilityController(this); + mTaskbarManager = new TaskbarManager( + this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized. @@ -618,8 +694,9 @@ public class TouchInteractionService extends Service { private void initInputMonitor(String reason) { disposeEventHandlers("Initializing input monitor due to: " + reason); - if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get() - || mTrackpadsConnected.isEmpty())) { + if (mDeviceState.isButtonNavMode() + && !mDeviceState.supportsAssistantGestureInButtonNav() + && (mTrackpadsConnected.isEmpty())) { return; } @@ -685,8 +762,8 @@ public class TouchInteractionService extends Service { private void onOverviewTargetChange(boolean isHomeAndOverviewSame) { mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame); - StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface() - .getCreatedContainer(); + StatefulActivity newOverviewActivity = + mOverviewComponentObserver.getActivityInterface().getCreatedContainer(); if (newOverviewActivity != null) { mTaskbarManager.setActivity(newOverviewActivity); } @@ -694,23 +771,14 @@ public class TouchInteractionService extends Service { } private PendingIntent createAllAppsPendingIntent() { - if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { - return new PendingIntent(new IIntentSender.Stub() { - @Override - public void send(int code, Intent intent, String resolvedType, - IBinder allowlistToken, IIntentReceiver finishedReceiver, - String requiredPermission, Bundle options) { - MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps()); - } - }); - } else { - return PendingIntent.getActivity( - this, - GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, - new Intent(mOverviewComponentObserver.getHomeIntent()) - .setAction(INTENT_ACTION_ALL_APPS_TOGGLE), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } + return new PendingIntent(new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, + IBinder allowlistToken, IIntentReceiver finishedReceiver, + String requiredPermission, Bundle options) { + MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps()); + } + }); } @UiThread @@ -750,6 +818,7 @@ public class TouchInteractionService extends Service { mTrackpadsConnected.clear(); mTaskbarManager.destroy(); + mDesktopVisibilityController.onDestroy(); sConnected = false; ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener); @@ -785,14 +854,28 @@ public class TouchInteractionService extends Service { TestLogging.recordMotionEvent( TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event); - boolean isUserUnlocked = LockedUserState.get(this).isUserUnlocked(); - if (!isUserUnlocked || (mDeviceState.isButtonNavMode() - && !isTrackpadMotionEvent(event))) { + if (!LockedUserState.get(this).isUserUnlocked()) { + ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ") + .append("Cannot process input event: user is locked")); + return; + } + + NavigationMode currentNavMode = mDeviceState.getMode(); + if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) { + ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ") + .append("Navigation mode switched mid-gesture (") + .append(mGestureStartNavMode.name()) + .append(" -> ") + .append(currentNavMode.name()) + .append("); cancelling gesture."), + NAVIGATION_MODE_SWITCHED); + event.setAction(ACTION_CANCEL); + } else if (mDeviceState.isButtonNavMode() + && !mDeviceState.supportsAssistantGestureInButtonNav() + && !isTrackpadMotionEvent(event)) { ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ") .append("Cannot process input event: ") - .append(!isUserUnlocked - ? "user is locked" - : "using 3-button nav and event is not a trackpad event")); + .append("using 3-button nav and event is not a trackpad event")); return; } @@ -819,6 +902,12 @@ public class TouchInteractionService extends Service { } } + if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { + mGestureStartNavMode = currentNavMode; + } else if (action == ACTION_UP || action == ACTION_CANCEL) { + mGestureStartNavMode = null; + } + SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent"); CompoundString reasonString = action == ACTION_DOWN @@ -829,11 +918,29 @@ public class TouchInteractionService extends Service { boolean isOneHandedModeActive = mDeviceState.isOneHandedModeActive(); boolean isInSwipeUpTouchRegion = mRotationTouchHelper.isInSwipeUpTouchRegion(event); TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); + BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null; + boolean isOnBubbles = bubbleControllers != null + && BubbleBarInputConsumer.isEventOnBubbles(tac, event); if (isInSwipeUpTouchRegion && tac != null) { tac.closeKeyboardQuickSwitchView(); } - if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) - || isHoverActionWithoutConsumer) { + if (mDeviceState.isButtonNavMode() + && mDeviceState.supportsAssistantGestureInButtonNav()) { + reasonString.append("in three button mode which supports Assistant gesture"); + // Consume gesture event for Assistant (all other gestures should do nothing). + if (mDeviceState.canTriggerAssistantAction(event)) { + reasonString.append(" and event can trigger assistant action") + .append(", consuming gesture for assistant action"); + mGestureState = + createGestureState(mGestureState, getTrackpadGestureType(event)); + mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event); + } else { + reasonString.append(" but event cannot trigger Assistant") + .append(", consuming gesture as no-op"); + mUncheckedConsumer = InputConsumer.NO_OP; + } + } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) + || isHoverActionWithoutConsumer || isOnBubbles) { reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion ? "one handed mode is not active and event is in swipe up region" : "isHoverActionWithoutConsumer == true") @@ -854,8 +961,7 @@ public class TouchInteractionService extends Service { : "event is a trackpad multi-finger swipe") .append(" and event can trigger assistant action") .append(", consuming gesture for assistant action"); - mGestureState = createGestureState(mGestureState, - getTrackpadGestureType(event)); + mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event)); // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we // should not interrupt it. QuickSwitch assumes that interruption can only // happen if the next gesture is also quick switch. @@ -1013,6 +1119,15 @@ public class TouchInteractionService extends Service { private InputConsumer newConsumer( GestureState previousGestureState, GestureState newGestureState, MotionEvent event) { + TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); + BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null; + if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) { + InputConsumer consumer = new BubbleBarInputConsumer(this, bubbleControllers, + mInputMonitorCompat); + logInputConsumerSelectionReason(consumer, newCompoundString( + "event is on bubbles, creating new input consumer")); + return consumer; + } AnimatedFloat progressProxy = mSwipeUpProxyProvider.apply(mGestureState); if (progressProxy != null) { InputConsumer consumer = new ProgressDelegateInputConsumer( @@ -1077,7 +1192,6 @@ public class TouchInteractionService extends Service { } // If Taskbar is present, we listen for swipe or cursor hover events to unstash it. - TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); if (tac != null && !(base instanceof AssistantInputConsumer)) { // Present always on large screen or on small screen w/ flag boolean useTaskbarConsumer = tac.getDeviceProfile().isTaskbarPresent @@ -1108,7 +1222,8 @@ public class TouchInteractionService extends Service { NavHandle navHandle = tac != null ? tac.getNavHandle() : SystemUiProxy.INSTANCE.get(this); if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning() - && navHandle.canNavHandleBeLongPressed()) { + && navHandle.canNavHandleBeLongPressed() + && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) { reasonString.append(NEWLINE_PREFIX) .append(reasonPrefix) .append(SUBSTRING_PREFIX) @@ -1140,7 +1255,7 @@ public class TouchInteractionService extends Service { getBaseContext(), mDeviceState, mInputMonitorCompat); } - if (ENABLE_TRACKPAD_GESTURE.get() && mGestureState.isTrackpadGesture() + if (mGestureState.isTrackpadGesture() && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) { reasonString = newCompoundString(reasonPrefix) .append(SUBSTRING_PREFIX) @@ -1204,6 +1319,11 @@ public class TouchInteractionService extends Service { return new CompoundString(NEWLINE_PREFIX).append(substring); } + private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) { + return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress() + && gestureState.isThreeFingerTrackpadGesture(); + } + private void logInputConsumerSelectionReason( InputConsumer consumer, CompoundString reasonString) { ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ") @@ -1538,7 +1658,6 @@ public class TouchInteractionService extends Service { pw.println("\tmConsumer=" + mConsumer.getName()); ActiveGestureLog.INSTANCE.dump("", pw); RecentsModel.INSTANCE.get(this).dump("", pw); - TopTaskTracker.INSTANCE.get(this).dump("", pw); if (mTaskAnimationManager != null) { mTaskAnimationManager.dump("", pw); } @@ -1546,6 +1665,7 @@ public class TouchInteractionService extends Service { createdOverviewActivity.getDeviceProfile().dump(this, "", pw); } mTaskbarManager.dumpLogs("", pw); + mDesktopVisibilityController.dumpLogs("", pw); pw.println("AssistStateManager:"); AssistStateManager.INSTANCE.get(this).dump("\t", pw); SystemUiProxy.INSTANCE.get(this).dump(pw); diff --git a/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java b/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java new file mode 100644 index 0000000000..d470b88d44 --- /dev/null +++ b/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java @@ -0,0 +1,44 @@ +/* + * 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.contextualeducation; + +import android.content.Context; + +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; +import com.android.quickstep.SystemUiProxy; +import com.android.systemui.contextualeducation.GestureType; + +/** + * A class to update contextual education data via {@link SystemUiProxy} + */ +public class SystemContextualEduStatsManager extends ContextualEduStatsManager { + private Context mContext; + + public SystemContextualEduStatsManager(Context context) { + mContext = context; + } + + @Override + public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) { + SystemUiProxy.INSTANCE.get(mContext).updateContextualEduStats(isTrackpadGesture, + gestureType.name()); + } + + @Override + public void close() { + } +} diff --git a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java new file mode 100644 index 0000000000..08345b85f6 --- /dev/null +++ b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.dagger; + +import dagger.Module; + +@Module +public class QuickStepModule { +} diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java new file mode 100644 index 0000000000..f2d571554d --- /dev/null +++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java @@ -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.dagger; + +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherBaseAppComponent; +import com.android.quickstep.logging.SettingsChangeLogger; + +/** + * Launcher Quickstep base component for Dagger injection. + * + * This class is not actually annotated as a Dagger component, since it is not used directly as one. + * Doing so generates unnecessary code bloat. + * + * See {@link LauncherAppComponent} for the one actually used. + */ +public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent { + SettingsChangeLogger getSettingsChangeLogger(); +} diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index f4a2738fef..e67a9bc1b6 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -253,6 +253,9 @@ public class FallbackRecentsView extends RecentsView mTouchSlop || Math.abs(dX) > mTouchSlop; + } + if (mStashedOrCollapsedOnDown && !mSwipeUpOnBubbleHandle && mPassedTouchSlop) { + boolean verticalGesture = Math.abs(dY) > Math.abs(dX); + if (verticalGesture && !mBubbleDragController.isDragging()) { + mSwipeUpOnBubbleHandle = true; + mBubbleStashController.showBubbleBar(/* expandBubbles= */ true); + // Bubbles is handling the swipe so make sure no one else gets it. + TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers"); + mInputMonitorCompat.pilferPointers(); + } + } + break; + case MotionEvent.ACTION_UP: + boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap; + if (isWithinTapTime && !mSwipeUpOnBubbleHandle && !mPassedTouchSlop + && mStashedOrCollapsedOnDown) { + // Taps on the handle / collapsed state should open the bar + mBubbleStashController.showBubbleBar(/* expandBubbles= */ true); + } + break; + } + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + cleanupAfterMotionEvent(); + } + } + + private void cleanupAfterMotionEvent() { + mPassedTouchSlop = false; + mSwipeUpOnBubbleHandle = false; + } + + private boolean isCollapsed() { + return mBubbleStashController.isBubbleBarVisible() + && !mBubbleBarViewController.isExpanded(); + } + + /** + * Returns whether the event is occurring on a visible bubble bar or the bar handle. + */ + public static boolean isEventOnBubbles(TaskbarActivityContext tac, MotionEvent ev) { + if (tac == null || !tac.isBubbleBarEnabled()) { + return false; + } + BubbleControllers controllers = tac.getBubbleControllers(); + if (controllers == null || !controllers.bubbleBarViewController.hasBubbles()) { + return false; + } + if (controllers.bubbleStashController.isStashed() + && controllers.bubbleStashedHandleViewController.isPresent()) { + return controllers.bubbleStashedHandleViewController.get().isEventOverHandle(ev); + } else if (controllers.bubbleBarViewController.isBubbleBarVisible()) { + return controllers.bubbleBarViewController.isEventOverBubbleBar(ev); + } + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java index f2643641e0..b66d4cba33 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java @@ -24,7 +24,6 @@ import static com.android.launcher3.util.VelocityUtils.PX_PER_MS; import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; import android.animation.Animator; @@ -58,7 +57,6 @@ import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.TransformParams.BuilderProxy; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputMonitorCompat; import java.util.HashMap; @@ -213,15 +211,13 @@ public class DeviceLockedInputConsumer implements InputConsumer, // This will come back and cancel the interaction. startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null, TAG); mHomeLaunched = true; - } else if (ENABLE_SHELL_TRANSITIONS) { - if (mTaskAnimationManager.getCurrentCallbacks() != null) { - if (mRecentsAnimationController != null) { - finishRecentsAnimationForShell(dismissTask); - } else { - // the transition of recents animation hasn't started, wait for it - mCancelWhenRecentsStart = true; - mDismissTask = dismissTask; - } + } else if (mTaskAnimationManager.getCurrentCallbacks() != null) { + if (mRecentsAnimationController != null) { + finishRecentsAnimationForShell(dismissTask); + } else { + // the transition of recents animation hasn't started, wait for it + mCancelWhenRecentsStart = true; + mDismissTask = dismissTask; } } mStateCallback.setState(STATE_HANDLER_INVALIDATED); @@ -278,9 +274,7 @@ public class DeviceLockedInputConsumer implements InputConsumer, } private void endRemoteAnimation() { - if (mHomeLaunched) { - ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false); - } else if (mRecentsAnimationController != null) { + if (!mHomeLaunched && mRecentsAnimationController != null) { mRecentsAnimationController.finishController( false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java index 848a43afb1..f4d36953b5 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java @@ -27,6 +27,8 @@ import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.DisplayController; @@ -48,7 +50,7 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled( NAV_HANDLE_LONG_PRESS); - private final NavHandleLongPressHandler mNavHandleLongPressHandler; + private NavHandleLongPressHandler mNavHandleLongPressHandler; private final float mNavHandleWidth; private final float mScreenWidth; @@ -73,17 +75,32 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { super(delegate, inputMonitor); mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x; mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress(); - int twoStageMultiplier = DeviceConfigWrapper.get().getTwoStageMultiplier(); AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context); if (assistStateManager.getLPNHDurationMillis().isPresent()) { mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue(); } else { mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); } - mOuterLongPressTimeout = mLongPressTimeout * twoStageMultiplier; - mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop(); - mTouchSlopSquared = mTouchSlopSquaredOriginal; - mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier); + float twoStageDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + mOuterLongPressTimeout = (int) (mLongPressTimeout * twoStageDurationMultiplier); + + float gestureNavTouchSlopSquared = deviceState.getSquaredTouchSlop(); + float twoStageSlopMultiplier = + (DeviceConfigWrapper.get().getTwoStageSlopPercentage() / 100f); + float twoStageSlopMultiplierSquared = twoStageSlopMultiplier * twoStageSlopMultiplier; + if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) { + // For 2 stages, the outer touch slop should match gesture nav. + mTouchSlopSquared = gestureNavTouchSlopSquared * twoStageSlopMultiplierSquared; + mOuterTouchSlopSquared = gestureNavTouchSlopSquared; + } else { + // For single stage, the touch slop should match gesture nav. + mTouchSlopSquared = gestureNavTouchSlopSquared; + // Note: This outer slop is not actually used for single-stage (flag disabled). + mOuterTouchSlopSquared = gestureNavTouchSlopSquared; + } + mTouchSlopSquaredOriginal = mTouchSlopSquared; + mGestureState = gestureState; mGestureState.setIsInExtendedSlopRegion(false); if (DEBUG_NAV_HANDLE) { @@ -250,4 +267,9 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { protected String getDelegatorName() { return "NavHandleLongPressInputConsumer"; } + + @VisibleForTesting + void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) { + mNavHandleLongPressHandler = navHandleLongPressHandler; + } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 0d450c6b15..69d3bc91cd 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -70,6 +70,9 @@ import java.util.function.Consumer; */ public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer { + private static final String TAG = "OtherActivityInputConsumer"; + private static final boolean DEBUG = true; + public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN"; private static final String UP_EVT = "OtherActivityInputConsumer.UP"; @@ -230,6 +233,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC // Start the window animation on down to give more time for launcher to draw if the // user didn't start the gesture over the back button + if (DEBUG) { + Log.d(TAG, "ACTION_DOWN: mIsDeferredDownTarget=" + mIsDeferredDownTarget); + } if (!mIsDeferredDownTarget) { startTouchTrackingForWindowAnimation(ev.getEventTime()); } @@ -284,9 +290,18 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC float horizontalDist = Math.abs(displacementX); float upDist = -displacement; - boolean passedSlop = mGestureState.isTrackpadGesture() - || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop - && !mGestureState.isInExtendedSlopRegion()); + boolean isTrackpadGesture = mGestureState.isTrackpadGesture(); + float squaredHypot = squaredHypot(displacementX, displacementY); + boolean isInExtendedSlopRegion = !mGestureState.isInExtendedSlopRegion(); + boolean passedSlop = isTrackpadGesture + || (squaredHypot >= mSquaredTouchSlop + && isInExtendedSlopRegion); + if (DEBUG) { + Log.d(TAG, "ACTION_MOVE: passedSlop=" + passedSlop + + " ( " + isTrackpadGesture + + " || (" + squaredHypot + " >= " + mSquaredTouchSlop + + " && " + isInExtendedSlopRegion + " ))"); + } if (!mPassedSlopOnThisGesture && passedSlop) { mPassedSlopOnThisGesture = true; @@ -306,6 +321,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange; + if (DEBUG) { + Log.d(TAG, "ACTION_MOVE: mPassedPilferInputSlop=" + mPassedPilferInputSlop); + } if (!mPassedPilferInputSlop) { if (passedSlop) { // Horizontal gesture is not allowed in this region @@ -391,9 +409,14 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs); mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished); mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener()); + mMotionPauseDetector.setIsTrackpadGesture(mGestureState.isTrackpadGesture()); mInteractionHandler.initWhenReady( "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation"); + if (DEBUG) { + Log.d(TAG, "startTouchTrackingForWindowAnimation: isRecentsAnimationRunning=" + + mTaskAnimationManager.isRecentsAnimationRunning()); + } if (mTaskAnimationManager.isRecentsAnimationRunning()) { mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState); mActiveCallbacks.removeListener(mCleanupHandler); @@ -422,6 +445,11 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC */ private void finishTouchTracking(MotionEvent ev) { TraceHelper.INSTANCE.beginSection(UP_EVT); + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: mPassedWindowMoveSlop=" + mPassedWindowMoveSlop); + Log.d(TAG, "finishTouchTracking: mInteractionHandler=" + mInteractionHandler); + Log.d(TAG, "finishTouchTracking: ev=" + ev); + } boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL; if (mPassedWindowMoveSlop && mInteractionHandler != null) { @@ -444,7 +472,14 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC // Since we start touch tracking on DOWN, we may reach this state without actually // starting the gesture. In that case, we need to clean-up an unfinished or un-started // animation. + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: mActiveCallbacks=" + mActiveCallbacks); + } if (mActiveCallbacks != null && mInteractionHandler != null) { + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: isRecentsAnimationRunning=" + + mTaskAnimationManager.isRecentsAnimationRunning()); + } if (mTaskAnimationManager.isRecentsAnimationRunning()) { // The animation started, but with no movement, in this case, there will be no // animateToProgress so we have to manually finish here. In the case of @@ -535,7 +570,13 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC public void onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets) { + if (DEBUG) { + Log.d(TAG, "FinishImmediatelyHandler: queuing callback"); + } Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> { + if (DEBUG) { + Log.d(TAG, "FinishImmediatelyHandler: running callback"); + } controller.finish(false /* toRecents */, null); }); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java index 95295b0eb9..5ad55ae177 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java @@ -15,9 +15,7 @@ */ package com.android.quickstep.inputconsumers; -import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_MOVE; -import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.INVALID_POINTER_ID; import static com.android.launcher3.Flags.enableCursorHoverStates; @@ -43,12 +41,12 @@ import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarThresholdUtils; import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; -import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.launcher3.touch.OverScroll; import com.android.launcher3.util.DisplayController; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.OverviewCommandHelper.CommandType; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -69,9 +67,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { private final int mTaskbarNavThresholdY; private final boolean mIsTaskbarAllAppsOpen; private boolean mHasPassedTaskbarNavThreshold; - private boolean mIsInBubbleBarArea; - private boolean mIsVerticalGestureOverBubbleBar; - private boolean mIsPassedBubbleBarSlop; private final int mTouchSlop; private final PointF mDownPos = new PointF(); @@ -159,9 +154,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) { mTransitionCallback.onActionDown(); } - if (mIsTransientTaskbar && isInBubbleBarArea(x)) { - mIsInBubbleBarArea = true; - } break; case MotionEvent.ACTION_POINTER_UP: int ptrIdx = ev.getActionIndex(); @@ -185,18 +177,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { float dX = mLastPos.x - mDownPos.x; float dY = mLastPos.y - mDownPos.y; - if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) { - boolean passedSlop = - Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop; - if (passedSlop) { - mIsPassedBubbleBarSlop = true; - mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX); - if (mIsVerticalGestureOverBubbleBar) { - setActive(ev); - } - } - } - if (mIsTransientTaskbar) { boolean passedTaskbarNavThreshold = dY < 0 && Math.abs(dY) >= mTaskbarNavThreshold; @@ -204,11 +184,7 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold && !mGestureState.isInExtendedSlopRegion()) { mHasPassedTaskbarNavThreshold = true; - if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) { - mTaskbarActivityContext.onSwipeToOpenBubblebar(); - } else { - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); - } + mTaskbarActivityContext.onSwipeToUnstashTaskbar(true); } if (dY < 0) { @@ -225,46 +201,13 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { break; case MotionEvent.ACTION_BUTTON_RELEASE: if (isStashedTaskbarHovered) { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME); + mOverviewCommandHelper.addCommand(CommandType.HOME); } break; } } - boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE; if (!isStashedTaskbarHovered) { - // if we're moving in the bubble bar area but we haven't passed the slop yet, don't - // propagate to the delegate, until we can determine the direction of the gesture. - if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) { - mDelegate.onMotionEvent(ev); - } - } - } else if (mIsVerticalGestureOverBubbleBar) { - // if we get here then this gesture is a vertical swipe over the bubble bar. - // we're also active and there's no need to delegate any additional motion events. the - // rest of the gesture will be handled here. - switch (ev.getAction()) { - case ACTION_MOVE: - int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == INVALID_POINTER_ID) { - break; - } - mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); - - float dY = mLastPos.y - mDownPos.y; - - // bubble bar swipe gesture uses the same threshold as the taskbar. - boolean passedTaskbarNavThreshold = dY < 0 - && Math.abs(dY) >= mTaskbarNavThreshold; - - if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) { - mHasPassedTaskbarNavThreshold = true; - mTaskbarActivityContext.onSwipeToOpenBubblebar(); - } - break; - case ACTION_UP: - case ACTION_CANCEL: - cleanupAfterMotionEvent(); - break; + mDelegate.onMotionEvent(ev); } } } @@ -301,9 +244,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { mTransitionCallback.onActionEnd(); } mHasPassedTaskbarNavThreshold = false; - mIsInBubbleBarArea = false; - mIsVerticalGestureOverBubbleBar = false; - mIsPassedBubbleBarSlop = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -313,22 +253,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { mMotionMoveCount = 0; } - private boolean isInBubbleBarArea(float x) { - if (mTaskbarActivityContext == null || !mIsTransientTaskbar) { - return false; - } - BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers(); - if (controllers == null) { - return false; - } - if (controllers.bubbleStashController.isStashed()) { - return controllers.bubbleStashedHandleViewController.containsX((int) x); - } else { - Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds(); - return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right; - } - } - /** * Listen for hover events for the stashed taskbar. * @@ -363,7 +287,7 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { // start a single unstash timeout if hovering bottom edge under the hinted taskbar. if (!sUnstashHandler.hasMessagesOrCallbacks()) { sUnstashHandler.postDelayed(() -> { - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); + mTaskbarActivityContext.onSwipeToUnstashTaskbar(false); mIsStashedTaskbarHovered = false; }, HOVER_TASKBAR_UNSTASH_TIMEOUT); } @@ -391,7 +315,7 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { startStashedTaskbarHover(/* isHovered = */ true); } else if (mBottomEdgeBounds.contains(x, y)) { // If hover screen's bottom edge not below the stashed taskbar, unstash it. - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); + mTaskbarActivityContext.onSwipeToUnstashTaskbar(false); } } diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java index 1bec97075a..f7f3157230 100644 --- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java +++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java @@ -62,6 +62,7 @@ import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; @@ -103,6 +104,9 @@ public class AllSetActivity extends Activity { private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate); + private final InvariantDeviceProfile.OnIDPChangeListener mOnIDPChangeListener = + modelPropertiesChanged -> updateHint(); + private TISBindHelper mTISBindHelper; private BgDrawable mBackground; @@ -115,6 +119,8 @@ public class AllSetActivity extends Activity { private AnimatorPlaybackController mLauncherStartAnim = null; + private TextView mHintView; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -167,12 +173,9 @@ public class AllSetActivity extends Activity { } }); - TextView hint = findViewById(R.id.hint); - DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this); - if (!dp.isGestureMode) { - hint.setText(R.string.allset_button_hint); - } - hint.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate()); + mHintView = findViewById(R.id.hint); + mHintView.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate()); + updateHint(); mTISBindHelper = new TISBindHelper(this, this::onTISConnected); @@ -190,7 +193,21 @@ public class AllSetActivity extends Activity { LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary), getTheme()); - startBackgroundAnimation(dp.isTablet); + startBackgroundAnimation(getDP().isTablet); + getIDP().addOnChangeListener(mOnIDPChangeListener); + } + + private InvariantDeviceProfile getIDP() { + return LauncherAppState.getInstance(this).getInvariantDeviceProfile(); + } + + private DeviceProfile getDP() { + return getIDP().getDeviceProfile(this); + } + + private void updateHint() { + mHintView.setText( + getDP().isGestureMode ? R.string.allset_hint : R.string.allset_button_hint); } private void runOnUiHelperThread(Runnable runnable) { @@ -202,7 +219,7 @@ public class AllSetActivity extends Activity { } private void startBackgroundAnimation(boolean forTablet) { - if (!Utilities.ATLEAST_S || mVibrator == null) { + if (mVibrator == null) { return; } boolean supportsThud = mVibrator.areAllPrimitivesSupported( @@ -311,6 +328,7 @@ public class AllSetActivity extends Activity { @Override protected void onDestroy() { super.onDestroy(); + getIDP().removeOnChangeListener(mOnIDPChangeListener); mTISBindHelper.onDestroy(); clearBinderOverride(); if (mBackgroundAnimatorListener != null) { diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java index 742b0fc7e8..7a86db37c7 100644 --- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java +++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -175,11 +173,8 @@ public class AnimatedTaskView extends ConstraintLayout { void setFakeTaskViewFillColor(@ColorInt int colorResId) { mFullTaskView.setBackgroundColor(colorResId); - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()){ - mTopTaskView.getBackground().setTint(colorResId); - mBottomTaskView.getBackground().setTint(colorResId); - } + mTopTaskView.getBackground().setTint(colorResId); + mBottomTaskView.getBackground().setTint(colorResId); } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java index 6757cd88cc..be7f8e5fc2 100644 --- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java @@ -15,7 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE; @@ -40,35 +39,29 @@ final class BackGestureTutorialController extends TutorialController { BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) { super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Back gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack, - ".surfaceBack", fragment.mRootView.mColorSurfaceBack, - ".secondaryBack", fragment.mRootView.mColorSecondaryBack)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack, + ".surfaceBack", fragment.mRootView.mColorSurfaceBack, + ".secondaryBack", fragment.mRootView.mColorSecondaryBack)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceBack - : fragment.mRootView.mColorSecondaryBack, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceBack + : fragment.mRootView.mColorSecondaryBack, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_tutorial_title - : R.string.back_gesture_intro_title; + return R.string.back_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_tutorial_subtitle - : R.string.back_gesture_intro_subtitle; + return R.string.back_gesture_tutorial_subtitle; } @Override @@ -85,9 +78,7 @@ final class BackGestureTutorialController extends TutorialController { public int getSuccessFeedbackSubtitle() { return mTutorialFragment.isAtFinalStep() ? R.string.back_gesture_feedback_complete_without_follow_up - : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_feedback_complete_with_follow_up - : R.string.back_gesture_feedback_complete_with_overview_follow_up; + : R.string.back_gesture_feedback_complete_with_follow_up; } @Override @@ -128,20 +119,12 @@ final class BackGestureTutorialController extends TutorialController { @LayoutRes int getMockAppTaskCurrentPageLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.back_gesture_tutorial_background - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_conversation - : R.layout.gesture_tutorial_mock_conversation; + return R.layout.back_gesture_tutorial_background; } @LayoutRes int getMockAppTaskPreviousPageLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.back_gesture_tutorial_background - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_conversation_list - : R.layout.gesture_tutorial_mock_conversation_list; + return R.layout.back_gesture_tutorial_background; } @Override @@ -214,17 +197,13 @@ final class BackGestureTutorialController extends TutorialController { } private void handleBackAttempt(BackGestureResult result) { - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - resetViewsForBackGesture(); - } + resetViewsForBackGesture(); switch (result) { case BACK_COMPLETED_FROM_LEFT: case BACK_COMPLETED_FROM_RIGHT: mTutorialFragment.releaseFeedbackAnimation(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mExitingAppView.setVisibility(View.GONE); - } + mExitingAppView.setVisibility(View.GONE); updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId()); showSuccessFeedback(); break; diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java index 1b12be85df..700fbf8d03 100644 --- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java +++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.content.Context; import android.content.res.Resources; import android.graphics.Point; @@ -211,10 +209,8 @@ public class EdgeBackGestureHandler implements OnTouchListener { } } - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x, - ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel()); - } + mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x, + ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel()); // forward touch mEdgeBackPanel.onMotionEvent(ev); diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java index 36ea9269b4..bc5cc15328 100644 --- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java +++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java @@ -37,10 +37,10 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.StatsLogManager; import com.android.quickstep.TouchInteractionService.TISBinder; import com.android.quickstep.interaction.TutorialController.TutorialType; +import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.TISBindHelper; import java.util.ArrayList; @@ -54,8 +54,6 @@ public class GestureSandboxActivity extends FragmentActivity { static final String KEY_TUTORIAL_TYPE = "tutorial_type"; static final String KEY_GESTURE_COMPLETE = "gesture_complete"; static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu"; - public static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95; - public static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05; @Nullable private TutorialType[] mTutorialSteps; private GestureSandboxFragment mCurrentFragment; @@ -80,9 +78,7 @@ public class GestureSandboxActivity extends FragmentActivity { Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState; boolean gestureComplete = args != null && args.getBoolean(KEY_GESTURE_COMPLETE, false); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - && args != null - && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) { + if (args != null && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) { mTutorialSteps = null; TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE); mCurrentFragment = tutorialTypeOverride == null @@ -102,9 +98,7 @@ public class GestureSandboxActivity extends FragmentActivity { .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment) .commit(); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - correctUserOrientation(); - } + correctUserOrientation(); mTISBindHelper = new TISBindHelper(this, this::onTISConnected); initWindowInsets(); @@ -116,9 +110,7 @@ public class GestureSandboxActivity extends FragmentActivity { super.onConfigurationChanged(newConfig); // Ensure the prompt to rotate the screen is updated - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - correctUserOrientation(); - } + correctUserOrientation(); } private void initWindowInsets() { @@ -170,10 +162,7 @@ public class GestureSandboxActivity extends FragmentActivity { getApplicationContext()).getDeviceProfile(this); if (deviceProfile.isTablet) { // The tutorial will work in either orientation if the height and width are similar - boolean isAspectRatioSquare = - deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND - && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND; - boolean showRotationPrompt = !isAspectRatioSquare + boolean showRotationPrompt = !LayoutUtils.isAspectRatioSquare(deviceProfile.aspectRatio) && getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index 1129e025c5..bf4eaf2f53 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.graphics.PointF; import com.android.launcher3.R; @@ -34,35 +32,29 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Home gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome, - ".surfaceHome", fragment.mRootView.mColorSurfaceHome, - ".secondaryHome", fragment.mRootView.mColorSecondaryHome)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome, + ".surfaceHome", fragment.mRootView.mColorSurfaceHome, + ".secondaryHome", fragment.mRootView.mColorSecondaryHome)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceHome - : fragment.mRootView.mColorSecondaryHome, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceHome + : fragment.mRootView.mColorSecondaryHome, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_title - : R.string.home_gesture_intro_title; + return R.string.home_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_subtitle - : R.string.home_gesture_intro_subtitle; + return R.string.home_gesture_tutorial_subtitle; } @Override @@ -72,9 +64,7 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll @Override public int getSuccessFeedbackTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_success - : R.string.gesture_tutorial_nice; + return R.string.home_gesture_tutorial_success; } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index a04dd4456b..e45f8d868e 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -16,7 +16,6 @@ package com.android.quickstep.interaction; import static com.android.app.animation.Interpolators.ACCELERATE; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -49,34 +48,28 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Overview gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview, - ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview, - ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview, + ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview, + ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceOverview - : fragment.mRootView.mColorSecondaryOverview, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceOverview + : fragment.mRootView.mColorSecondaryOverview, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_title - : R.string.overview_gesture_intro_title; + return R.string.overview_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_subtitle - : R.string.overview_gesture_intro_subtitle; + return R.string.overview_gesture_tutorial_subtitle; } @Override @@ -86,9 +79,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont @Override public int getSuccessFeedbackTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_success - : R.string.gesture_tutorial_nice; + return R.string.overview_gesture_tutorial_success; } @Override @@ -168,10 +159,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont @Override protected int getMockPreviousAppTaskThumbnailColor() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? mTutorialFragment.mRootView.mColorSurfaceContainer - : mContext.getResources().getColor( - R.color.gesture_tutorial_fake_previous_task_view_color); + return mTutorialFragment.mRootView.mColorSurfaceContainer; } @Override @@ -224,11 +212,8 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont case OVERVIEW_GESTURE_COMPLETED: setGestureCompleted(); mTutorialFragment.releaseFeedbackAnimation(); - animateTaskViewToOverview(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()); + animateTaskViewToOverview(true); onMotionPaused(true /*arbitrary value*/); - if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - showSuccessFeedback(); - } break; case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: diff --git a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java index affedb9497..d733267d37 100644 --- a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java +++ b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java @@ -15,16 +15,12 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Insets; -import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; -import android.view.View; import android.view.WindowInsets; import android.widget.RelativeLayout; @@ -39,10 +35,6 @@ import com.android.launcher3.Utilities; /** Root layout that TutorialFragment uses to intercept motion events. */ public class RootSandboxLayout extends RelativeLayout { - private final Rect mTempStepIndicatorBounds = new Rect(); - private final Rect mTempInclusionBounds = new Rect(); - private final Rect mTempExclusionBounds = new Rect(); - @ColorInt final int mColorSurfaceContainer; @ColorInt final int mColorOnSurfaceHome; @ColorInt final int mColorSurfaceHome; @@ -54,11 +46,6 @@ public class RootSandboxLayout extends RelativeLayout { @ColorInt final int mColorSurfaceOverview; @ColorInt final int mColorSecondaryOverview; - private View mFeedbackView; - private View mTutorialStepView; - private View mSkipButton; - private View mDoneButton; - public RootSandboxLayout(Context context) { this(context, null); } @@ -123,56 +110,4 @@ public class RootSandboxLayout extends RelativeLayout { return getHeight() + insets.top + insets.bottom; } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - return; - } - mFeedbackView = findViewById(R.id.gesture_tutorial_fragment_feedback_view); - mTutorialStepView = - mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); - mSkipButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_close_button); - mDoneButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_action_button); - - mFeedbackView.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (mSkipButton.getVisibility() != VISIBLE - && mDoneButton.getVisibility() != VISIBLE) { - return; - } - // Either the skip or the done button is ever shown at once, never both. - boolean showingSkipButton = mSkipButton.getVisibility() == VISIBLE; - boolean isRTL = Utilities.isRtl(getContext().getResources()); - updateTutorialStepViewTranslation( - showingSkipButton ? mSkipButton : mDoneButton, - // Translate the step indicator away from whichever button is being - // shown. The skip button in on the left in LTR or on the right in RTL. - // The done button is on the right in LTR or left in RTL. - (showingSkipButton && !isRTL) || (!showingSkipButton && isRTL)); - }); - } - - private void updateTutorialStepViewTranslation( - @NonNull View anchorView, boolean translateToRight) { - mTempStepIndicatorBounds.set( - mTutorialStepView.getLeft(), - mTutorialStepView.getTop(), - mTutorialStepView.getRight(), - mTutorialStepView.getBottom()); - mTempInclusionBounds.set(0, 0, mFeedbackView.getWidth(), mFeedbackView.getHeight()); - mTempExclusionBounds.set( - anchorView.getLeft(), - anchorView.getTop(), - anchorView.getRight(), - anchorView.getBottom()); - - Utilities.translateOverlappingView( - mTutorialStepView, - mTempStepIndicatorBounds, - mTempInclusionBounds, - mTempExclusionBounds, - translateToRight ? Utilities.TRANSLATE_RIGHT : Utilities.TRANSLATE_LEFT); - } } diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java index ad13efbd42..e462706c98 100644 --- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java @@ -48,7 +48,6 @@ import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; import com.android.quickstep.GestureState; import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationDeviceState; @@ -127,9 +126,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { void resetTaskViews() { mFakeHotseatView.setVisibility(View.INVISIBLE); mFakeIconView.setVisibility(View.INVISIBLE); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFakeIconView.getBackground().setTint(getFakeTaskViewColor()); - } + mFakeIconView.getBackground().setTint(getFakeTaskViewColor()); if (mTutorialFragment.getActivity() != null) { int height = mTutorialFragment.getRootView().getFullscreenHeight(); int width = mTutorialFragment.getRootView().getWidth(); @@ -138,9 +135,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mFakeTaskViewRadius = 0; mFakeTaskView.invalidateOutline(); mFakeTaskView.setVisibility(View.VISIBLE); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); - } + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); mFakeTaskView.setAlpha(1); mFakePreviousTaskView.setVisibility(View.INVISIBLE); mFakePreviousTaskView.setAlpha(1); @@ -390,12 +385,10 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { false, /* isOpening */ mFakeIconView, mDp); mFakeIconView.setAlpha(1); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - int iconColor = ColorUtils.blendARGB( - getFakeTaskViewColor(), getHotseatIconColor(), progress); - mFakeIconView.getBackground().setTint(iconColor); - mFakeTaskView.setBackgroundColor(iconColor); - } + int iconColor = ColorUtils.blendARGB( + getFakeTaskViewColor(), getHotseatIconColor(), progress); + mFakeIconView.getBackground().setTint(iconColor); + mFakeTaskView.setBackgroundColor(iconColor); mFakeTaskView.setAlpha(getWindowAlpha(progress)); mFakePreviousTaskView.setAlpha(getWindowAlpha(progress)); } diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index f89888a113..5028da4f6a 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -19,10 +19,7 @@ import static android.view.View.GONE; import static android.view.View.NO_ID; import static android.view.View.inflate; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; @@ -33,7 +30,6 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Rect; -import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.RippleDrawable; import android.util.Log; @@ -52,7 +48,6 @@ import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; @@ -153,8 +148,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); - mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? null : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view); + mFakeTaskbarView = null; mFakePreviousTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); @@ -165,32 +159,30 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); mSkipTutorialDialog = createSkipTutorialDialog(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); - mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); - mAnimatedGestureDemonstration = rootView.findViewById( - R.id.gesture_demonstration_animations); - mExitingAppView = rootView.findViewById(R.id.exiting_app_back); - mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx; - mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx; - mExitingAppMargin = mContext.getResources().getDimensionPixelSize( - R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); - mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); - mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( - R.dimen.gesture_tutorial_back_gesture_end_corner_radius); - mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( - this::createScalingMatrix); + mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); + mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); + mAnimatedGestureDemonstration = rootView.findViewById( + R.id.gesture_demonstration_animations); + mExitingAppView = rootView.findViewById(R.id.exiting_app_back); + mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx; + mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx; + mExitingAppMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); + mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); + mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( + R.dimen.gesture_tutorial_back_gesture_end_corner_radius); + mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( + this::createScalingMatrix); - mFeedbackTitleView.setText(getIntroductionTitle()); - mFeedbackSubtitleView.setText(getIntroductionSubtitle()); - mExitingAppView.setClipToOutline(true); - mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(mExitingAppRect, mExitingAppRadius); - } - }); - } + mFeedbackTitleView.setText(getIntroductionTitle()); + mFeedbackSubtitleView.setText(getIntroductionSubtitle()); + mExitingAppView.setClipToOutline(true); + mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(mExitingAppRect, mExitingAppRadius); + } + }); mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_FOCUSED); @@ -261,19 +253,11 @@ abstract class TutorialController implements BackGestureAttemptCallback, @LayoutRes protected int getMockHotseatResId() { - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - return mTutorialFragment.isLargeScreen() - ? mTutorialFragment.isFoldable() - ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat - : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat - : R.layout.redesigned_gesture_tutorial_mock_hotseat; - } else { - return mTutorialFragment.isLargeScreen() - ? mTutorialFragment.isFoldable() - ? R.layout.gesture_tutorial_foldable_mock_hotseat - : R.layout.gesture_tutorial_tablet_mock_hotseat - : R.layout.gesture_tutorial_mock_hotseat; - } + return mTutorialFragment.isLargeScreen() + ? mTutorialFragment.isFoldable() + ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat + : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat + : R.layout.redesigned_gesture_tutorial_mock_hotseat; } @LayoutRes @@ -312,9 +296,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, @DrawableRes public int getMockAppIconResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.drawable.redesigned_hotseat_icon - : R.drawable.default_sandbox_app_icon; + return R.drawable.redesigned_hotseat_icon; } @DrawableRes @@ -374,11 +356,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackView.setTranslationY(0); return; } - Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); - AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); - if (gestureAnimation != null && edgeAnimation != null) { - playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); - } + playFeedbackAnimation(); } /** @@ -442,12 +420,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, } mFeedbackTitleView.setText(titleResId); - mFeedbackSubtitleView.setText( - ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID - ? mContext.getText(subtitleResId) - : Utilities.wrapForTts( - mContext.getText(subtitleResId), - mContext.getString(spokenSubtitleResId))); + mFeedbackSubtitleView.setText(subtitleResId); if (isGestureSuccessful) { if (mTutorialFragment.isAtFinalStep()) { showActionButton(); @@ -458,27 +431,16 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeTaskViewCallback = null; } - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - showSuccessPage(); - } + showSuccessPage(); } mGestureCompleted = isGestureSuccessful; - - Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); - AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); - if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { - playFeedbackAnimation( - gestureAnimation, - edgeAnimation, - mShowFeedbackRunnable, - useGestureAnimationDelay); - return; + if (!isGestureSuccessful) { + playFeedbackAnimation(); } else { mTutorialFragment.releaseFeedbackAnimation(); + mFeedbackViewCallback = mShowFeedbackRunnable; + mFeedbackView.post(mFeedbackViewCallback); } - mFeedbackViewCallback = mShowFeedbackRunnable; - - mFeedbackView.post(mFeedbackViewCallback); } private void showSuccessPage() { @@ -517,79 +479,17 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackTitleView.removeCallbacks(mTitleViewCallback); } - private void playFeedbackAnimation( - @NonNull Animator gestureAnimation, - @NonNull AnimatedVectorDrawable edgeAnimation, - @NonNull Runnable onStartRunnable, - boolean useGestureAnimationDelay) { - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFeedbackView.setVisibility(View.VISIBLE); - mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); - mFullGestureDemonstration.setVisibility(View.VISIBLE); - mAnimatedGestureDemonstration.playAnimation(); - return; - } - - if (gestureAnimation.isRunning()) { - gestureAnimation.cancel(); - } - if (edgeAnimation.isRunning()) { - edgeAnimation.reset(); - } - gestureAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - - mEdgeGestureVideoView.setVisibility(GONE); - if (edgeAnimation.isRunning()) { - edgeAnimation.stop(); - } - - if (!useGestureAnimationDelay) { - onStartRunnable.run(); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - - mEdgeGestureVideoView.setVisibility(View.VISIBLE); - edgeAnimation.start(); - - gestureAnimation.removeListener(this); - } - }); - - cancelQueuedGestureAnimation(); - if (useGestureAnimationDelay) { - mFeedbackViewCallback = onStartRunnable; - mFakeTaskViewCallback = gestureAnimation::start; - - mFeedbackView.post(mFeedbackViewCallback); - mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); - } else { - gestureAnimation.start(); - } + private void playFeedbackAnimation() { + mFeedbackView.setVisibility(View.VISIBLE); + mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); + mFullGestureDemonstration.setVisibility(View.VISIBLE); + mAnimatedGestureDemonstration.playAnimation(); } void setRippleHotspot(float x, float y) { mRippleDrawable.setHotspot(x, y); } - void showRippleEffect(@Nullable Runnable onCompleteRunnable) { - mRippleDrawable.setState( - new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled}); - mRippleView.postDelayed(() -> { - mRippleDrawable.setState(new int[] {}); - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - }, RIPPLE_VISIBLE_MS); - } - void onActionButtonClicked(View button) { mTutorialFragment.continueTutorial(); } @@ -601,24 +501,19 @@ abstract class TutorialController implements BackGestureAttemptCallback, updateDrawables(); updateLayout(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance()); - mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance()); - mDoneButton.getBackground().setTint(getDoneButtonColor()); - mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() - ? R.raw.checkmark_animation_end - : R.raw.checkmark_animation_in_progress); - if (!isGestureCompleted()) { - mCheckmarkAnimation.setVisibility(GONE); - startGestureAnimation(); - if (mTutorialType == TutorialType.BACK_NAVIGATION) { - resetViewsForBackGesture(); - } - + mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance()); + mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance()); + mDoneButton.getBackground().setTint(getDoneButtonColor()); + mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() + ? R.raw.checkmark_animation_end + : R.raw.checkmark_animation_in_progress); + if (!isGestureCompleted()) { + mCheckmarkAnimation.setVisibility(GONE); + startGestureAnimation(); + if (mTutorialType == TutorialType.BACK_NAVIGATION) { + resetViewsForBackGesture(); } - } else { - hideFeedback(); - hideActionButton(); + } mGestureCompleted = false; @@ -654,13 +549,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark); } - void hideActionButton() { - mSkipButton.setVisibility(View.VISIBLE); - // Invisible to maintain the layout. - mDoneButton.setVisibility(View.INVISIBLE); - mDoneButton.setOnClickListener(null); - } - void showActionButton() { mSkipButton.setVisibility(GONE); mDoneButton.setVisibility(View.VISIBLE); @@ -711,10 +599,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, } private void updateSubtext() { - if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mTutorialStepView.setTutorialProgress( - mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); - } + mTutorialStepView.setTutorialProgress( + mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); } private void updateHotseatChildViewColor(@Nullable View child) { @@ -727,9 +613,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( mContext, getMockWallpaperResId())); mTutorialFragment.updateFeedbackAnimation(); - mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? getFakeLauncherColor() - : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); + mFakeLauncherView.setBackgroundColor(getFakeLauncherColor()); updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); mFakeTaskView.animate().alpha(1).setListener( @@ -738,19 +622,15 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeIconView.setBackground(AppCompatResources.getDrawable( mContext, getMockAppIconResId())); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mExitingAppView.setBackgroundColor(getExitingAppColor()); - mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); - updateHotseatChildViewColor(mHotseatIconView); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); - } else { - updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); - } + mExitingAppView.setBackgroundColor(getExitingAppColor()); + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); + updateHotseatChildViewColor(mHotseatIconView); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); } } @@ -782,7 +662,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams(); if (!mTutorialFragment.isLargeScreen()) { DeviceProfile dp = mTutorialFragment.getDeviceProfile(); - dp.updateIsSeascape(mContext); hotseatLayoutParams.addRule(dp.isLandscape ? (dp.isSeascape() diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index 0fafb94d1e..2ff2c836ae 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -17,7 +17,6 @@ package com.android.quickstep.interaction; import static android.view.View.NO_ID; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU; @@ -215,9 +214,7 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc super.onCreateView(inflater, container, savedInstanceState); mRootView = (RootSandboxLayout) inflater.inflate( - ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.redesigned_gesture_tutorial_fragment - : R.layout.gesture_tutorial_fragment, + R.layout.redesigned_gesture_tutorial_fragment, container, false); @@ -383,10 +380,7 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc if (mTutorialController != null && !isGestureComplete()) { mTutorialController.hideFeedback(); } - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mTutorialController.pauseAndHideLottieAnimation(); - } + mTutorialController.pauseAndHideLottieAnimation(); // Note: Using logical-or to ensure both functions get called. return mEdgeBackGestureHandler.onTouch(view, motionEvent) diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java index 5ac04da0ac..995635fa6c 100644 --- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java +++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java @@ -39,19 +39,25 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Xml; -import com.android.launcher3.AutoInstallsLayout; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.DeviceGridState; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; -import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.ExecutorUtil; import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SettingsCache; +import com.android.quickstep.dagger.QuickstepBaseAppComponent; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -59,9 +65,12 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Optional; +import javax.inject.Inject; + /** * Utility class to log launcher settings changes */ +@LauncherAppSingleton public class SettingsChangeLogger implements DisplayController.DisplayInfoChangeListener, OnSharedPreferenceChangeListener, SafeCloseable { @@ -69,11 +78,10 @@ public class SettingsChangeLogger implements /** * Singleton instance */ - public static MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(SettingsChangeLogger::new); + public static DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSettingsChangeLogger); private static final String TAG = "SettingsChangeLogger"; - private static final String ROOT_TAG = "androidx.preference.PreferenceScreen"; private static final String BOOLEAN_PREF = "SwitchPreference"; private final Context mContext; @@ -84,20 +92,31 @@ public class SettingsChangeLogger implements private StatsLogManager.LauncherEvent mNotificationDotsEvent; private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent; - private SettingsChangeLogger(Context context) { + @Inject + SettingsChangeLogger(@ApplicationContext Context context, DaggerSingletonTracker tracker) { + this(context, StatsLogManager.newInstance(context), tracker); + } + + @VisibleForTesting + SettingsChangeLogger(Context context, StatsLogManager statsLogManager, + DaggerSingletonTracker tracker) { mContext = context; - mStatsLogManager = StatsLogManager.newInstance(mContext); + mStatsLogManager = statsLogManager; mLoggablePrefs = loadPrefKeys(context); - DisplayController.INSTANCE.get(context).addChangeListener(this); - mNavMode = DisplayController.getNavigationMode(context); - getPrefs(context).registerOnSharedPreferenceChangeListener(this); - getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this); + ExecutorUtil.executeSyncOnMainOrFail(() -> { + DisplayController.INSTANCE.get(context).addChangeListener(this); + mNavMode = DisplayController.getNavigationMode(context); - SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context); - mSettingsCache.register(NOTIFICATION_BADGING_URI, - this::onNotificationDotsChanged); - onNotificationDotsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI)); + getPrefs(context).registerOnSharedPreferenceChangeListener(this); + getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this); + + SettingsCache settingsCache = SettingsCache.INSTANCE.get(context); + settingsCache.register(NOTIFICATION_BADGING_URI, + this::onNotificationDotsChanged); + onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI)); + tracker.addCloseable(this); + }); } private static ArrayMap loadPrefKeys(Context context) { @@ -105,7 +124,13 @@ public class SettingsChangeLogger implements ArrayMap result = new ArrayMap<>(); try { - AutoInstallsLayout.beginDocument(parser, ROOT_TAG); + // Move cursor to first tag because it could be + // androidx.preference.PreferenceScreen or PreferenceScreen + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG + && eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG @@ -189,13 +214,21 @@ public class SettingsChangeLogger implements prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff)); } + @VisibleForTesting + ArrayMap getLoggingPrefs() { + return mLoggablePrefs; + } + @Override public void close() { getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); + SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext); + settingsCache.unregister(NOTIFICATION_BADGING_URI, this::onNotificationDotsChanged); } - private static class LoggablePref { + @VisibleForTesting + static class LoggablePref { public boolean defaultValue; public int eventIdOn; public int eventIdOff; diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt index ec04cb767d..88ef0a8bcb 100644 --- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt @@ -37,6 +37,7 @@ import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import androidx.core.util.component1 import androidx.core.util.component2 +import androidx.core.view.updateLayoutParams import com.android.launcher3.DeviceProfile import com.android.launcher3.Flags import com.android.launcher3.LauncherAnimUtils @@ -242,7 +243,30 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { lp.height = ViewGroup.LayoutParams.WRAP_CONTENT } - override fun getDwbLayoutTranslations( + override fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View + ) { + banner.pivotX = 0f + banner.pivotY = 0f + banner.rotation = degreesRotated + banner.updateLayoutParams { + gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START + width = + if (isGroupedTaskView) { + snapshotViewHeight + } else { + taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx + } + } + } + + override fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, splitBounds: SplitBounds?, @@ -252,39 +276,25 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { banner: View ): Pair { val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams - val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL val translationX = banner.height.toFloat() - - val bannerParams = banner.layoutParams as FrameLayout.LayoutParams - bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START - banner.pivotX = 0f - banner.pivotY = 0f - banner.rotation = degreesRotated - - if (splitBounds == null) { - // Single, fullscreen case - bannerParams.width = taskViewHeight - snapshotParams.topMargin - return Pair(translationX, snapshotParams.topMargin.toFloat()) - } - - // Set correct width and translations val translationY: Float - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].measuredHeight + if (splitBounds == null) { translationY = snapshotParams.topMargin.toFloat() } else { - bannerParams.width = thumbnailViews[1].measuredHeight - val topLeftTaskPlusDividerPercent = - if (splitBounds.appsStackedVertically) { - splitBounds.topTaskPercent + splitBounds.dividerHeightPercent - } else { - splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent - } - translationY = - snapshotParams.topMargin + - (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent + if (desiredTaskId == splitBounds.leftTopTaskId) { + translationY = snapshotParams.topMargin.toFloat() + } else { + val topLeftTaskPlusDividerPercent = + if (splitBounds.appsStackedVertically) { + splitBounds.topTaskPercent + splitBounds.dividerHeightPercent + } else { + splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent + } + translationY = + snapshotParams.topMargin + + (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent + } } - return Pair(translationX, translationY) } @@ -300,6 +310,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { if (isRtl) displacement < 0 else displacement > 0 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1 + /* -------------------- */ override fun getChildBounds( @@ -442,6 +453,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { } } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake landscape. + */ override fun measureGroupedTaskViewThumbnailBounds( primarySnapshot: View, secondarySnapshot: View, @@ -449,7 +464,8 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { parentHeight: Int, splitBoundsConfig: SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean ) { val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams @@ -558,6 +574,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { iconAppChipView.setRotation(degreesRotated) } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake landscape. + */ override fun setSplitIconParams( primaryIconView: View, secondaryIconView: View, @@ -568,7 +588,8 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { groupedTaskViewWidth: Int, isRtl: Boolean, deviceProfile: DeviceProfile, - splitConfig: SplitBounds + splitConfig: SplitBounds, + inSplitSelection: Boolean ) { val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java index eeacee1cf6..c0b697daf8 100644 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java @@ -243,54 +243,54 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements } @Override - public Pair getDwbLayoutTranslations(int taskViewWidth, - int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, - View[] thumbnailViews, int desiredTaskId, View banner) { - float translationX = 0; - float translationY = 0; + public void updateDwbBannerLayout(int taskViewWidth, int taskViewHeight, + boolean isGroupedTaskView, @NonNull DeviceProfile deviceProfile, + int snapshotViewWidth, int snapshotViewHeight, @NonNull View banner) { FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); banner.setPivotX(0); banner.setPivotY(0); banner.setRotation(getDegreesRotated()); - if (splitBounds == null) { - // Single, fullscreen case + if (isGroupedTaskView) { + bannerParams.gravity = + BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL); + bannerParams.width = snapshotViewWidth; + } else { bannerParams.width = MATCH_PARENT; bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL; - return new Pair<>(translationX, translationY); } + banner.setLayoutParams(bannerParams); + } - bannerParams.gravity = - BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL); - - // Set correct width - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].getMeasuredWidth(); - } else { - bannerParams.width = thumbnailViews[1].getMeasuredWidth(); - } - - // Set translations - if (deviceProfile.isLeftRightSplit) { - if (desiredTaskId == splitBounds.rightBottomTaskId) { - float leftTopTaskPercent = splitBounds.appsStackedVertically - ? splitBounds.topTaskPercent - : splitBounds.leftTaskPercent; - float dividerThicknessPercent = splitBounds.appsStackedVertically - ? splitBounds.dividerHeightPercent - : splitBounds.dividerWidthPercent; - translationX = ((taskViewWidth * leftTopTaskPercent) - + (taskViewWidth * dividerThicknessPercent)); - } - } else { - if (desiredTaskId == splitBounds.leftTopTaskId) { - FrameLayout.LayoutParams snapshotParams = - (FrameLayout.LayoutParams) thumbnailViews[0] - .getLayoutParams(); - float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically - ? (1f - splitBounds.topTaskPercent) - : (1f - splitBounds.leftTaskPercent); - translationY = -((taskViewHeight - snapshotParams.topMargin) - * bottomRightTaskPlusDividerPercent); + @NonNull + @Override + public Pair getDwbBannerTranslations(int taskViewWidth, + int taskViewHeight, SplitBounds splitBounds, @NonNull DeviceProfile deviceProfile, + @NonNull View[] thumbnailViews, int desiredTaskId, @NonNull View banner) { + float translationX = 0; + float translationY = 0; + if (splitBounds != null) { + if (deviceProfile.isLeftRightSplit) { + if (desiredTaskId == splitBounds.rightBottomTaskId) { + float leftTopTaskPercent = splitBounds.appsStackedVertically + ? splitBounds.topTaskPercent + : splitBounds.leftTaskPercent; + float dividerThicknessPercent = splitBounds.appsStackedVertically + ? splitBounds.dividerHeightPercent + : splitBounds.dividerWidthPercent; + translationX = ((taskViewWidth * leftTopTaskPercent) + + (taskViewWidth * dividerThicknessPercent)); + } + } else { + if (desiredTaskId == splitBounds.leftTopTaskId) { + FrameLayout.LayoutParams snapshotParams = + (FrameLayout.LayoutParams) thumbnailViews[0] + .getLayoutParams(); + float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically + ? (1f - splitBounds.topTaskPercent) + : (1f - splitBounds.leftTaskPercent); + translationY = -((taskViewHeight - snapshotParams.topMargin) + * bottomRightTaskPlusDividerPercent); + } } } return new Pair<>(translationX, translationY); @@ -530,49 +530,63 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements } } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. If true, we have custom translations/scaling in place + * for the remaining snapshot, so we'll skip setting translation/scale + * here. + */ @Override public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, - DeviceProfile dp, boolean isRtl) { + DeviceProfile dp, boolean isRtl, boolean inSplitSelection) { int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; + + FrameLayout.LayoutParams primaryParams = + (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); + FrameLayout.LayoutParams secondaryParams = + (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); + + // Reset margins that aren't used in this method, but are used in other + // `RecentsPagedOrientationHandler` variants. + secondaryParams.topMargin = 0; + primaryParams.topMargin = spaceAboveSnapshot; + int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; float dividerScale = splitBoundsConfig.appsStackedVertically ? splitBoundsConfig.dividerHeightPercent : splitBoundsConfig.dividerWidthPercent; Pair taskViewSizes = getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight); - if (dp.isLeftRightSplit) { - int scaledDividerBar = Math.round(parentWidth * dividerScale); - if (isRtl) { - int translationX = taskViewSizes.second.x + scaledDividerBar; - primarySnapshot.setTranslationX(-translationX); - secondarySnapshot.setTranslationX(0); + if (!inSplitSelection) { + // Reset translations that aren't used in this method, but are used in other + // `RecentsPagedOrientationHandler` variants. + primarySnapshot.setTranslationY(0); + + if (dp.isLeftRightSplit) { + int scaledDividerBar = Math.round(parentWidth * dividerScale); + if (isRtl) { + int translationX = taskViewSizes.second.x + scaledDividerBar; + primarySnapshot.setTranslationX(-translationX); + secondarySnapshot.setTranslationX(0); + } else { + int translationX = taskViewSizes.first.x + scaledDividerBar; + secondarySnapshot.setTranslationX(translationX); + primarySnapshot.setTranslationX(0); + } + secondarySnapshot.setTranslationY(spaceAboveSnapshot); } else { - int translationX = taskViewSizes.first.x + scaledDividerBar; - secondarySnapshot.setTranslationX(translationX); + float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); + float translationY = + taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight; + secondarySnapshot.setTranslationY(translationY); + + // Reset unused translations. + secondarySnapshot.setTranslationX(0); primarySnapshot.setTranslationX(0); } - secondarySnapshot.setTranslationY(spaceAboveSnapshot); - - // Reset unused translations - primarySnapshot.setTranslationY(0); - } else { - float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); - float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight; - secondarySnapshot.setTranslationY(translationY); - - FrameLayout.LayoutParams primaryParams = - (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); - FrameLayout.LayoutParams secondaryParams = - (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); - secondaryParams.topMargin = 0; - primaryParams.topMargin = spaceAboveSnapshot; - - // Reset unused translations - primarySnapshot.setTranslationY(0); - secondarySnapshot.setTranslationX(0); - primarySnapshot.setTranslationX(0); } + primarySnapshot.measure( View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)); @@ -580,10 +594,6 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)); - primarySnapshot.setScaleX(1); - secondarySnapshot.setScaleX(1); - primarySnapshot.setScaleY(1); - secondarySnapshot.setScaleY(1); } @Override @@ -660,11 +670,16 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements iconAppChipView.setRotation(getDegreesRotated()); } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. If true, we have custom translations in place for the + * remaining icon, so we'll skip setting translations here. + */ @Override public void setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, - DeviceProfile deviceProfile, SplitBounds splitConfig) { + DeviceProfile deviceProfile, SplitBounds splitConfig, boolean inSplitSelection) { FrameLayout.LayoutParams primaryIconParams = (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu() @@ -678,20 +693,23 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements secondaryIconParams.gravity = TOP | START; secondaryIconParams.topMargin = primaryIconParams.topMargin; secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart()); - if (deviceProfile.isLeftRightSplit) { - if (isRtl) { - int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth; - primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth); + if (!inSplitSelection) { + if (deviceProfile.isLeftRightSplit) { + if (isRtl) { + int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth; + primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth); + } else { + secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth); + } } else { - secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth); + primaryAppChipView.setSplitTranslationX(0); + secondaryAppChipView.setSplitTranslationX(0); + int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(), + splitConfig.visualDividerBounds.height()); + secondaryAppChipView.setSplitTranslationY( + primarySnapshotHeight + (deviceProfile.isTablet ? 0 + : dividerThickness)); } - } else { - primaryAppChipView.setSplitTranslationX(0); - secondaryAppChipView.setSplitTranslationX(0); - int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(), - splitConfig.visualDividerBounds.height()); - secondaryAppChipView.setSplitTranslationY( - primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness)); } } else if (deviceProfile.isLeftRightSplit) { // We calculate the "midpoint" of the thumbnail area, and place the icons there. @@ -714,46 +732,53 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements if (deviceProfile.isSeascape()) { primaryIconParams.gravity = TOP | (isRtl ? END : START); secondaryIconParams.gravity = TOP | (isRtl ? END : START); - if (splitConfig.initiatedFromSeascape) { - // if the split was initiated from seascape, - // the task on the right (secondary) is slightly larger - primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); - secondaryIconView.setTranslationX(bottomToMidpointOffset); - } else { - // if not, - // the task on the left (primary) is slightly larger - primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset - - taskIconHeight); - secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); + if (!inSplitSelection) { + if (splitConfig.initiatedFromSeascape) { + // if the split was initiated from seascape, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); + secondaryIconView.setTranslationX(bottomToMidpointOffset); + } else { + // if not, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset + - taskIconHeight); + secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); + } } } else { primaryIconParams.gravity = TOP | (isRtl ? START : END); secondaryIconParams.gravity = TOP | (isRtl ? START : END); - if (!splitConfig.initiatedFromSeascape) { - // if the split was initiated from landscape, - // the task on the left (primary) is slightly larger - primaryIconView.setTranslationX(-bottomToMidpointOffset); - secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); - } else { - // if not, - // the task on the right (secondary) is slightly larger - primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); - secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset - + taskIconHeight); + if (!inSplitSelection) { + if (!splitConfig.initiatedFromSeascape) { + // if the split was initiated from landscape, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationX(-bottomToMidpointOffset); + secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); + } else { + // if not, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); + secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset + + taskIconHeight); + } } } } else { primaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - // shifts icon half a width left (height is used here since icons are square) - primaryIconView.setTranslationX(-(taskIconHeight / 2f)); secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - secondaryIconView.setTranslationX(taskIconHeight / 2f); + if (!inSplitSelection) { + // shifts icon half a width left (height is used here since icons are square) + primaryIconView.setTranslationX(-(taskIconHeight / 2f)); + secondaryIconView.setTranslationX(taskIconHeight / 2f); + } } - if (!enableOverviewIconMenu()) { + if (!enableOverviewIconMenu() && !inSplitSelection) { primaryIconView.setTranslationY(0); secondaryIconView.setTranslationY(0); } + primaryIconView.setLayoutParams(primaryIconParams); secondaryIconView.setLayoutParams(secondaryIconParams); } diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt index df4b03038f..b8d0412eff 100644 --- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt @@ -184,7 +184,8 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { parentHeight: Int, splitBoundsConfig: SplitConfigurationOptions.SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean ) /** @@ -199,6 +200,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { parentWidth: Int, parentHeight: Int ): Pair + // Overview TaskMenuView methods /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */ fun setTaskIconParams( @@ -234,7 +236,8 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { groupedTaskViewWidth: Int, isRtl: Boolean, deviceProfile: DeviceProfile, - splitConfig: SplitConfigurationOptions.SplitBounds + splitConfig: SplitConfigurationOptions.SplitBounds, + inSplitSelection: Boolean ) /* @@ -294,13 +297,24 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { deviceProfile: DeviceProfile ) + /** Layout a Digital Wellbeing Banner on its parent. TaskView. */ + fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View + ) + /** - * Calculates the position where a Digital Wellbeing Banner should be placed on its parent + * Calculates the translations where a Digital Wellbeing Banner should be apply on its parent * TaskView. * * @return A Pair of Floats representing the proper x and y translations. */ - fun getDwbLayoutTranslations( + fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, splitBounds: SplitConfigurationOptions.SplitBounds?, @@ -309,6 +323,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { desiredTaskId: Int, banner: View ): Pair + // The following are only used by TaskViewTouchHandler. /** @return Either VERTICAL or HORIZONTAL. */ diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt index 333359fdd8..bc91911749 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt @@ -28,6 +28,7 @@ import android.view.View.MeasureSpec import android.widget.FrameLayout import androidx.core.util.component1 import androidx.core.util.component2 +import androidx.core.view.updateLayoutParams import com.android.launcher3.DeviceProfile import com.android.launcher3.Flags import com.android.launcher3.R @@ -125,7 +126,30 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { } } - override fun getDwbLayoutTranslations( + override fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View + ) { + banner.pivotX = 0f + banner.pivotY = 0f + banner.rotation = degreesRotated + banner.updateLayoutParams { + gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START + width = + if (isGroupedTaskView) { + snapshotViewHeight + } else { + taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx + } + } + } + + override fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, splitBounds: SplitBounds?, @@ -135,39 +159,26 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { banner: View ): Pair { val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams - val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL - - val bannerParams = banner.layoutParams as FrameLayout.LayoutParams - bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START - banner.pivotX = 0f - banner.pivotY = 0f - banner.rotation = degreesRotated - val translationX: Float = (taskViewWidth - banner.height).toFloat() - if (splitBounds == null) { - // Single, fullscreen case - bannerParams.width = taskViewHeight - snapshotParams.topMargin - return Pair(translationX, banner.height.toFloat()) - } - - // Set correct width and translations val translationY: Float - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].measuredHeight - val bottomRightTaskPlusDividerPercent = - if (splitBounds.appsStackedVertically) { - 1f - splitBounds.topTaskPercent - } else { - 1f - splitBounds.leftTaskPercent - } - translationY = - banner.height - - (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent - } else { - bannerParams.width = thumbnailViews[1].measuredHeight + if (splitBounds == null) { translationY = banner.height.toFloat() + } else { + if (desiredTaskId == splitBounds.leftTopTaskId) { + val bottomRightTaskPlusDividerPercent = + if (splitBounds.appsStackedVertically) { + 1f - splitBounds.topTaskPercent + } else { + 1f - splitBounds.leftTaskPercent + } + translationY = + banner.height - + (taskViewHeight - snapshotParams.topMargin) * + bottomRightTaskPlusDividerPercent + } else { + translationY = banner.height.toFloat() + } } - return Pair(translationX, translationY) } @@ -266,6 +277,10 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { iconAppChipView.setRotation(degreesRotated) } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake seascape. + */ override fun measureGroupedTaskViewThumbnailBounds( primarySnapshot: View, secondarySnapshot: View, @@ -273,7 +288,8 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { parentHeight: Int, splitBoundsConfig: SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean ) { val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams @@ -339,6 +355,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { if (isRtl) displacement > 0 else displacement < 0 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1 + /* -------------------- */ override fun getSplitIconsPosition( diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..df546ca776 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.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 + +import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback + +/** Notifies added callbacks that high res state has changed */ +interface HighResLoadingStateNotifier { + /** Adds a callback for high res loading state */ + fun addCallback(callback: HighResLoadingStateChangedCallback) + + /** Removes a callback for high res loading state */ + fun removeCallback(callback: HighResLoadingStateChangedCallback) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt index c1eef0b393..9c4248cec9 100644 --- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt @@ -17,6 +17,7 @@ package com.android.quickstep.recents.data import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData import kotlinx.coroutines.flow.Flow interface RecentTasksRepository { @@ -25,10 +26,16 @@ interface RecentTasksRepository { /** * Gets the data associated with a task that has id [taskId]. Flow will settle on null if the - * task was not found. + * task was not found. [Task.thumbnail] will settle on null if task is invisible. */ fun getTaskDataById(taskId: Int): Flow + /** + * Gets the [ThumbnailData] associated with a task that has id [taskId]. Flow will settle on + * null if the task was not found or is invisible. + */ + fun getThumbnailById(taskId: Int): Flow + /** * Sets the tasks that are visible, indicating that properties relating to visuals need to be * populated e.g. icons/thumbnails etc. diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt new file mode 100644 index 0000000000..d2cb595586 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +/** + * Container to hold [com.android.launcher3.DeviceProfile] related to Recents. + * + * @property isLargeScreen whether the current device posture has a large screen + */ +data class RecentsDeviceProfile( + val isLargeScreen: Boolean, +) diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt new file mode 100644 index 0000000000..13cf56d5b2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +interface RecentsDeviceProfileRepository { + fun getRecentsDeviceProfile(): RecentsDeviceProfile +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt new file mode 100644 index 0000000000..c64453d0cd --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import com.android.quickstep.views.RecentsViewContainer + +/** + * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains + * data related to Recents. + */ +class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewContainer) : + RecentsDeviceProfileRepository { + + override fun getRecentsDeviceProfile() = + with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt new file mode 100644 index 0000000000..2c2a744ed4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt @@ -0,0 +1,29 @@ +/* + * 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 + +/** + * Container to hold orientation/rotation related information related to Recents. + * + * @property activityRotation rotation of the activity hosting RecentsView + */ +data class RecentsRotationState( + @Surface.Rotation val activityRotation: Int, + @Surface.Rotation val orientationHandlerRotation: Int, +) diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt new file mode 100644 index 0000000000..ed074d2e08 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +interface RecentsRotationStateRepository { + fun getRecentsRotationState(): RecentsRotationState +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt new file mode 100644 index 0000000000..8417b061df --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt @@ -0,0 +1,34 @@ +/* + * 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.RecentsOrientedState + +/** + * Repository for [RecentsRotationState] which holds orientation/rotation related information + * related to Recents + */ +class RecentsRotationStateRepositoryImpl(private val state: RecentsOrientedState) : + RecentsRotationStateRepository { + override fun getRecentsRotationState() = + with(state) { + RecentsRotationState( + activityRotation = recentsActivityRotation, + orientationHandlerRotation = orientationHandler.rotation + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt new file mode 100644 index 0000000000..6e7789d7f8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.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 + +import com.android.quickstep.util.TaskVisualsChangeListener + +/** Notifies added listeners that task visuals have changed */ +interface TaskVisualsChangeNotifier { + /** Adds a listener for visuals changes */ + fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) + + /** Removes a listener for visuals changes */ + fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt new file mode 100644 index 0000000000..a45d194a1c --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt @@ -0,0 +1,145 @@ +/* + * 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.os.UserHandle +import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.quickstep.util.TaskVisualsChangeListener +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.concurrent.ConcurrentHashMap + +/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */ +interface TaskVisualsChangedDelegate : + TaskVisualsChangeListener, HighResLoadingStateChangedCallback { + /** Registers a callback for visuals relating to icons */ + fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback, + ) + + /** Unregisters a callback for visuals relating to icons */ + fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) + + /** Registers a callback for visuals relating to thumbnails */ + fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback, + ) + + /** Unregisters a callback for visuals relating to thumbnails */ + fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) + + /** A callback for task icon changes */ + interface TaskIconChangedCallback { + /** Informs the listener that the task icon has changed */ + fun onTaskIconChanged() + } + + /** A callback for task thumbnail changes */ + interface TaskThumbnailChangedCallback { + /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */ + fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) + + /** Informs the listener that the default resolution for loading thumbnails has changed */ + fun onHighResLoadingStateChanged() + } +} + +class TaskVisualsChangedDelegateImpl( + private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier, + private val highResLoadingStateNotifier: HighResLoadingStateNotifier, +) : TaskVisualsChangedDelegate { + private val taskIconChangedCallbacks = + ConcurrentHashMap>() + private val taskThumbnailChangedCallbacks = + ConcurrentHashMap>() + + override fun onTaskIconChanged(taskId: Int) { + taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskIconChanged(pkg: String, user: UserHandle) { + taskIconChangedCallbacks.values + .filter { (taskKey, _) -> + pkg == taskKey.packageName && user.identifier == taskKey.userId + } + .forEach { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? { + taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) -> + callback.onTaskThumbnailChanged(thumbnailData) + } + return null + } + + override fun onHighResLoadingStateChanged(enabled: Boolean) { + taskThumbnailChangedCallbacks.values.forEach { (_, callback) -> + callback.onHighResLoadingStateChanged() + } + } + + override fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback, + ) { + updateCallbacks { + taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback + } + } + + override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) { + updateCallbacks { taskIconChangedCallbacks.remove(taskKey.id) } + } + + override fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback, + ) { + updateCallbacks { + taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback + } + } + + override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) { + updateCallbacks { taskThumbnailChangedCallbacks.remove(taskKey.id) } + } + + @Synchronized + private fun updateCallbacks(callbackModifier: () -> Unit) { + val prevHasCallbacks = + taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0 + callbackModifier() + + val currHasCallbacks = + taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0 + + when { + prevHasCallbacks && !currHasCallbacks -> { + taskVisualsChangeNotifier.removeThumbnailChangeListener(this) + highResLoadingStateNotifier.removeCallback(this) + } + !prevHasCallbacks && currHasCallbacks -> { + taskVisualsChangeNotifier.addThumbnailChangeListener(this) + highResLoadingStateNotifier.addCallback(this) + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt index b21a1b4add..eb3c2d1217 100644 --- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -16,99 +16,199 @@ package com.android.quickstep.recents.data -import com.android.quickstep.TaskIconCache +import android.graphics.drawable.Drawable +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.quickstep.task.thumbnail.data.TaskIconDataSource import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource import com.android.quickstep.util.GroupTask import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) class TasksRepository( private val recentsModel: RecentTasksDataSource, private val taskThumbnailDataSource: TaskThumbnailDataSource, - private val taskIconCache: TaskIconCache, + private val taskIconDataSource: TaskIconDataSource, + private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate, + recentsCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, ) : RecentTasksRepository { private val groupedTaskData = MutableStateFlow(emptyList()) - private val _taskData = - groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } private val visibleTaskIds = MutableStateFlow(emptySet()) - private val taskData: Flow> = - combine(_taskData, getThumbnailQueryResults()) { tasks, results -> - tasks.forEach { task -> - // Add retrieved thumbnails + remove unnecessary thumbnails - task.thumbnail = results[task.key.id] - } - tasks + private val taskData = + groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } + private val visibleTasks = + combine(taskData, visibleTaskIds) { tasks, visibleIds -> + tasks.filter { it.key.id in visibleIds } } + private val iconQueryResults: Flow> = + visibleTasks + .map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) } + .flatMapLatest { iconRequestFlows: List -> + if (iconRequestFlows.isEmpty()) { + flowOf(emptyMap()) + } else { + combine(iconRequestFlows) { it.toMap() } + } + } + .distinctUntilChanged() + + private val thumbnailQueryResults: Flow> = + visibleTasks + .map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) } + .flatMapLatest { thumbnailRequestFlows: List -> + if (thumbnailRequestFlows.isEmpty()) { + flowOf(emptyMap()) + } else { + combine(thumbnailRequestFlows) { it.toMap() } + } + } + .distinctUntilChanged() + + private val augmentedTaskData: Flow> = + combine(taskData, thumbnailQueryResults, iconQueryResults) { + tasks, + thumbnailQueryResults, + iconQueryResults -> + tasks.onEach { task -> + // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible) + task.thumbnail = thumbnailQueryResults[task.key.id] + + // TODO(b/352331675) don't load icons for DesktopTaskView + // Add retrieved icons + remove unnecessary icons + val iconQueryResult = iconQueryResults[task.key.id] + task.icon = iconQueryResult?.icon + task.titleDescription = iconQueryResult?.contentDescription + task.title = iconQueryResult?.title + } + } + .flowOn(dispatcherProvider.io) + .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(), replay = 1) + override fun getAllTaskData(forceRefresh: Boolean): Flow> { if (forceRefresh) { recentsModel.getTasks { groupedTaskData.value = it } } - return taskData + return augmentedTaskData } override fun getTaskDataById(taskId: Int): Flow = - taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + augmentedTaskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + + override fun getThumbnailById(taskId: Int): Flow = + getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId } override fun setVisibleTasks(visibleTaskIdList: List) { this.visibleTaskIds.value = visibleTaskIdList.toSet() } - /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */ - private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = - flow { - emit(task.key.id to task.thumbnail) - val thumbnailDataResult: ThumbnailData? = - suspendCancellableCoroutine { continuation -> - val cancellableTask = - taskThumbnailDataSource.updateThumbnailInBackground(task) { - continuation.resume(it) - } - continuation.invokeOnCancellation { cancellableTask?.cancel() } + /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */ + private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow { + trySend(task.key.id to task.thumbnail) + trySend(task.key.id to getThumbnailFromDataSource(task)) + + val callback = + object : TaskThumbnailChangedCallback { + override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) { + trySend(task.key.id to thumbnailData) + } + + override fun onHighResLoadingStateChanged() { + launch { trySend(task.key.id to getThumbnailFromDataSource(task)) } + } + } + taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback) + awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) } + } + + /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */ + private fun getIconDataRequest(task: Task): IconDataRequest = + callbackFlow { + trySend(task.key.id to task.getTaskIconQueryResponse()) + trySend(task.key.id to getIconFromDataSource(task)) + + val callback = + object : TaskIconChangedCallback { + override fun onTaskIconChanged() { + launch { trySend(task.key.id to getIconFromDataSource(task)) } + } } - emit(task.key.id to thumbnailDataResult) + taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback) + awaitClose { + taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key) + } } .distinctUntilChanged() - /** - * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for - * each visible task. It then collects the responses and returns them in a Map as soon as they - * are available. - */ - private fun getThumbnailQueryResults(): Flow> { - val visibleTasks = - combine(_taskData, visibleTaskIds) { tasks, visibleIds -> - tasks.filter { it.key.id in visibleIds } - } - val visibleThumbnailDataRequests: Flow> = - visibleTasks.map { - it.map { visibleTask -> - val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail } - getThumbnailDataRequest(taskCopy) - } - } - return visibleThumbnailDataRequests.flatMapLatest { - thumbnailRequestFlows: List -> - if (thumbnailRequestFlows.isEmpty()) { - flowOf(emptyMap()) - } else { - combine(thumbnailRequestFlows) { it.toMap() } + private suspend fun getThumbnailFromDataSource(task: Task) = + withContext(dispatcherProvider.main) { + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskThumbnailDataSource.getThumbnailInBackground(task) { + continuation.resume(it) + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } + } + } + + private suspend fun getIconFromDataSource(task: Task) = + withContext(dispatcherProvider.main) { + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title + -> + icon.constantState?.let { + continuation.resume( + TaskIconQueryResponse( + it.newDrawable().mutate(), + contentDescription, + title + ) + ) + } + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } } } - } } -typealias ThumbnailDataRequest = Flow> +data class TaskIconQueryResponse( + val icon: Drawable, + val contentDescription: String, + val title: String +) + +private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? { + val iconVal = icon ?: return null + val titleDescriptionVal = titleDescription ?: return null + val titleVal = title ?: return null + + return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal) +} + +private typealias ThumbnailDataRequest = Flow> + +private typealias IconDataRequest = Flow> diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt new file mode 100644 index 0000000000..0a5544f36b --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt @@ -0,0 +1,296 @@ +/* + * 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.di + +import android.content.Context +import android.util.Log +import android.view.View +import com.android.launcher3.util.coroutines.ProductionDispatchers +import com.android.quickstep.RecentsModel +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate +import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl +import com.android.quickstep.recents.data.TasksRepository +import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.usecase.GetThumbnailUseCase +import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.thumbnail.SplashAlphaUseCase +import com.android.quickstep.task.thumbnail.TaskThumbnailViewData +import com.android.quickstep.task.viewmodel.TaskContainerData +import com.android.quickstep.task.viewmodel.TaskOverlayViewModel +import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel +import com.android.quickstep.task.viewmodel.TaskViewData +import com.android.quickstep.task.viewmodel.TaskViewModel +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.Task +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +internal typealias RecentsScopeId = String + +class RecentsDependencies private constructor(private val appContext: Context) { + private val scopes = mutableMapOf() + + init { + startDefaultScope(appContext) + } + + /** + * This function initialised the default scope with RecentsView dependencies. These dependencies + * are used multiple times and should be a singleton to share across Recents classes. + */ + private fun startDefaultScope(appContext: Context) { + createScope(DEFAULT_SCOPE_ID).apply { + set(RecentsViewData::class.java.simpleName, RecentsViewData()) + val recentsCoroutineScope = + CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView")) + set(CoroutineScope::class.java.simpleName, recentsCoroutineScope) + val recentsModel = RecentsModel.INSTANCE.get(appContext) + val taskVisualsChangedDelegate = + TaskVisualsChangedDelegateImpl( + recentsModel, + recentsModel.thumbnailCache.highResLoadingState + ) + set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate) + + // Create RecentsTaskRepository singleton + val recentTasksRepository: RecentTasksRepository = + with(recentsModel) { + TasksRepository( + this, + thumbnailCache, + iconCache, + taskVisualsChangedDelegate, + recentsCoroutineScope, + ProductionDispatchers + ) + } + set(RecentTasksRepository::class.java.simpleName, recentTasksRepository) + } + } + + inline fun inject( + scopeId: RecentsScopeId = "", + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, + ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory) + + @Suppress("UNCHECKED_CAST") + @JvmOverloads + fun inject( + modelClass: Class, + scopeId: RecentsScopeId = DEFAULT_SCOPE_ID, + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + factory: ((extras: RecentsDependenciesExtras) -> T)? = null, + ): T { + val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID } + val scope = scopes[currentScopeId] ?: createScope(currentScopeId) + + log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO) + var instance: T? + synchronized(this) { + instance = getDependency(scope, modelClass) + log("found instance? $instance", Log.INFO) + if (instance == null) { + instance = + factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras) + scope[modelClass.simpleName] = instance!! + } + } + return instance!! + } + + inline fun provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T = + provide(T::class.java, scopeId = scopeId, factory = factory) + + @JvmOverloads + fun provide( + modelClass: Class, + scopeId: RecentsScopeId = DEFAULT_SCOPE_ID, + factory: () -> T, + ) = inject(modelClass, scopeId, factory = { factory.invoke() }) + + private fun getDependency(scope: RecentsDependenciesScope, modelClass: Class): T? { + var instance: T? = scope[modelClass.simpleName] as T? + if (instance == null) { + instance = + scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId -> + getScope(scopeId)[modelClass.simpleName] + } as T? + } + if (instance != null) log("Found dependency: $instance", Log.INFO) + return instance + } + + fun getScope(scope: Any): RecentsDependenciesScope { + val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString() + return getScope(scopeId) + } + + fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope = + scopes[scopeId] ?: createScope(scopeId) + + // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely. + // Each class should be responsible for providing a factory function to create a new instance. + @Suppress("UNCHECKED_CAST") + private fun createDependency( + modelClass: Class, + scopeId: RecentsScopeId, + extras: RecentsDependenciesExtras, + ): T { + log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN) + val instance: Any = + when (modelClass) { + RecentTasksRepository::class.java -> { + with(RecentsModel.INSTANCE.get(appContext)) { + TasksRepository( + this, + thumbnailCache, + iconCache, + get(), + get(), + ProductionDispatchers + ) + } + } + RecentsViewData::class.java -> RecentsViewData() + TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras)) + TaskViewData::class.java -> { + val taskViewType = extras["TaskViewType"] as TaskViewType + TaskViewData(taskViewType) + } + TaskContainerData::class.java -> TaskContainerData() + TaskThumbnailViewData::class.java -> TaskThumbnailViewData() + TaskThumbnailViewModel::class.java -> + TaskThumbnailViewModel( + recentsViewData = inject(), + taskViewData = inject(scopeId, extras), + taskContainerData = inject(scopeId), + getThumbnailPositionUseCase = inject(), + tasksRepository = inject(), + splashAlphaUseCase = inject(scopeId), + ) + TaskOverlayViewModel::class.java -> { + val task = extras["Task"] as Task + TaskOverlayViewModel( + task = task, + recentsViewData = inject(), + recentTasksRepository = inject(), + getThumbnailPositionUseCase = inject() + ) + } + GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject()) + SysUiStatusNavFlagsUseCase::class.java -> + SysUiStatusNavFlagsUseCase(taskRepository = inject()) + GetThumbnailPositionUseCase::class.java -> + GetThumbnailPositionUseCase( + deviceProfileRepository = inject(), + rotationStateRepository = inject(), + tasksRepository = inject() + ) + SplashAlphaUseCase::class.java -> + SplashAlphaUseCase( + recentsViewData = inject(), + taskContainerData = inject(scopeId), + taskThumbnailViewData = inject(scopeId), + tasksRepository = inject(), + rotationStateRepository = inject(), + ) + else -> { + log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR) + error("Factory for ${modelClass.simpleName} not defined!") + } + } + return instance as T + } + + private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope { + return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it } + } + + private fun log(message: String, @Log.Level level: Int = Log.DEBUG) { + if (DEBUG) { + when (level) { + Log.WARN -> Log.w(TAG, message) + Log.VERBOSE -> Log.v(TAG, message) + Log.INFO -> Log.i(TAG, message) + Log.ERROR -> Log.e(TAG, message) + else -> Log.d(TAG, message) + } + } + } + + companion object { + private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope" + private const val TAG = "RecentsDependencies" + private const val DEBUG = false + + @Volatile private lateinit var instance: RecentsDependencies + + fun initialize(view: View): RecentsDependencies = initialize(view.context) + + fun initialize(context: Context): RecentsDependencies { + synchronized(this) { + if (!Companion::instance.isInitialized) { + instance = RecentsDependencies(context.applicationContext) + } + } + return instance + } + + fun getInstance(): RecentsDependencies { + if (!Companion::instance.isInitialized) { + throw UninitializedPropertyAccessException( + "Recents dependencies are not initialized. " + + "Call `RecentsDependencies.initialize` before using this container." + ) + } + return instance + } + + fun destroy() { + instance.scopes.clear() + instance.startDefaultScope(instance.appContext) + } + } +} + +inline fun RecentsDependencies.Companion.inject( + scope: Any = "", + vararg extras: Pair, + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): Lazy = lazy { get(scope, RecentsDependenciesExtras(extras), factory) } + +inline fun RecentsDependencies.Companion.get( + scope: Any = "", + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): T { + val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString() + return getInstance().inject(scopeId, extras, factory) +} + +inline fun RecentsDependencies.Companion.get( + scope: Any = "", + vararg extras: Pair, + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): T = get(scope, RecentsDependenciesExtras(extras), factory) + +fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId) diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt new file mode 100644 index 0000000000..753cb6e7c9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt @@ -0,0 +1,27 @@ +/* + * 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.di + +data class RecentsDependenciesExtras(private val data: MutableMap = mutableMapOf()) { + constructor(value: Array>) : this(value.toMap().toMutableMap()) + + operator fun get(key: String) = data[key] + + operator fun set(key: String, value: Any) { + data[key] = value + } +} diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt new file mode 100644 index 0000000000..56bb1edfe9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt @@ -0,0 +1,71 @@ +/* + * 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.di + +import android.util.Log + +class RecentsDependenciesScope( + val scopeId: RecentsScopeId, + private val dependencies: MutableMap = mutableMapOf(), + private val scopeIds: MutableList = mutableListOf() +) { + val scopeIdsLinked: List + get() = scopeIds.toList() + + operator fun get(identifier: String): Any? { + log("get $identifier") + return dependencies[identifier] + } + + operator fun set(key: String, value: Any) { + synchronized(this) { + log("set $key") + dependencies[key] = value + } + } + + fun remove(key: String): Any? { + synchronized(this) { + log("remove $key") + return dependencies.remove(key) + } + } + + fun linkTo(scope: RecentsDependenciesScope) { + log("linking to ${scope.scopeId}") + scopeIds += scope.scopeId + } + + fun close() { + log("reset") + synchronized(this) { dependencies.clear() } + } + + private fun log(message: String) { + if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message") + } + + override fun toString(): String = + "scopeId: $scopeId" + + "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" + + "\n linked to: ${scopeIds.joinToString(", ")}" + + private companion object { + private const val TAG = "RecentsDependenciesScope" + private const val DEBUG = false + } +} diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt new file mode 100644 index 0000000000..f060d7deac --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt @@ -0,0 +1,54 @@ +/* + * 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.usecase + +import android.graphics.Matrix +import android.graphics.Rect +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import kotlinx.coroutines.flow.firstOrNull + +/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */ +class GetThumbnailPositionUseCase( + private val deviceProfileRepository: RecentsDeviceProfileRepository, + private val rotationStateRepository: RecentsRotationStateRepository, + private val tasksRepository: RecentTasksRepository, + private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper() +) { + suspend fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState { + val thumbnailData = + tasksRepository.getThumbnailById(taskId).firstOrNull() ?: return MissingThumbnail + val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail + previewPositionHelper.updateThumbnailMatrix( + Rect(0, 0, thumbnail.width, thumbnail.height), + thumbnailData, + width, + height, + deviceProfileRepository.getRecentsDeviceProfile().isLargeScreen, + rotationStateRepository.getRecentsRotationState().activityRotation, + isRtl + ) + return MatrixScaling( + previewPositionHelper.matrix, + previewPositionHelper.isOrientationChanged + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt new file mode 100644 index 0000000000..3aa808ebd1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.usecase + +import android.graphics.Bitmap +import com.android.quickstep.recents.data.RecentTasksRepository +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking + +/** Use case for retrieving thumbnail. */ +class GetThumbnailUseCase(private val taskRepository: RecentTasksRepository) { + /** Returns the latest thumbnail associated with [taskId] if loaded, or null otherwise */ + fun run(taskId: Int): Bitmap? = runBlocking { + taskRepository.getThumbnailById(taskId).firstOrNull()?.thumbnail + } +} diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt new file mode 100644 index 0000000000..1d19c7d332 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.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.recents.usecase + +import android.view.WindowInsetsController +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.quickstep.recents.data.RecentTasksRepository +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking + +/** UseCase to calculate flags for status bar and navigation bar */ +class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) { + fun getSysUiStatusNavFlags(taskId: Int): Int { + val thumbnailData = + runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0 + + val thumbnailAppearance = thumbnailData.appearance + var flags = 0 + flags = + flags or + if ( + thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0 + ) + FLAG_LIGHT_STATUS + else FLAG_DARK_STATUS + flags = + flags or + if ( + thumbnailAppearance and + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0 + ) + FLAG_LIGHT_NAV + else FLAG_DARK_NAV + return flags + } +} diff --git a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt new file mode 100644 index 0000000000..1a1bef709d --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.usecase + +import android.graphics.Matrix + +/** State on how a task Thumbnail can fit on given canvas */ +sealed class ThumbnailPositionState { + data object MissingThumbnail : ThumbnailPositionState() + + data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState() +} diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt index 28212cf3a5..87446b032b 100644 --- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt @@ -24,4 +24,22 @@ class RecentsViewData { // This is typically a View concern but it is used to invalidate rendering in other Views val scale = MutableStateFlow(1f) + + // Whether the current RecentsView state supports task overlays. + // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM. + val overlayEnabled = MutableStateFlow(false) + + // The settled set of visible taskIds that is updated after RecentsView scroll settles. + val settledFullyVisibleTaskIds = MutableStateFlow(emptySet()) + + // Color tint on foreground scrim + val tintAmount = MutableStateFlow(0f) + + val thumbnailSplashProgress = MutableStateFlow(0f) + + // A list of taskIds that are associated with a RecentsAnimationController. */ + val runningTaskIds = MutableStateFlow(emptySet()) + + // Whether we should use static screenshot instead of live tile for taskIds in [runningTaskIds] + val runningTaskShowScreenshot = MutableStateFlow(false) } diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt new file mode 100644 index 0000000000..5cf68239b8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.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.quickstep.recents.viewmodel + +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first + +class RecentsViewModel( + private val recentsTasksRepository: RecentTasksRepository, + private val recentsViewData: RecentsViewData, +) { + fun refreshAllTaskData() { + recentsTasksRepository.getAllTaskData(true) + } + + fun updateVisibleTasks(visibleTaskIdList: List) { + recentsTasksRepository.setVisibleTasks(visibleTaskIdList) + } + + fun updateScale(scale: Float) { + recentsViewData.scale.value = scale + } + + fun updateFullscreenProgress(fullscreenProgress: Float) { + recentsViewData.fullscreenProgress.value = fullscreenProgress + } + + fun updateTasksFullyVisible(taskIds: Set) { + recentsViewData.settledFullyVisibleTaskIds.value = taskIds + } + + fun setOverlayEnabled(isOverlayEnabled: Boolean) { + recentsViewData.overlayEnabled.value = isOverlayEnabled + } + + fun setTintAmount(tintAmount: Float) { + recentsViewData.tintAmount.value = tintAmount + } + + fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) { + recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha + } + + suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map?) { + if (updatedThumbnails.isNullOrEmpty()) return + combine( + updatedThumbnails.map { + recentsTasksRepository.getThumbnailById(it.key).filter { thumbnailData -> + thumbnailData?.snapshotId == it.value.snapshotId + } + } + ) {} + .first() + } + + suspend fun waitForRunningTaskShowScreenshotToUpdate() { + recentsViewData.runningTaskShowScreenshot.filter { it }.first() + } + + fun onReset() { + updateVisibleTasks(emptyList()) + } + + fun updateRunningTask(taskIds: Set) { + recentsViewData.runningTaskIds.value = taskIds + } + + fun setRunningTaskShowScreenshot(showScreenshot: Boolean) { + recentsViewData.runningTaskShowScreenshot.value = showScreenshot + } +} diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt new file mode 100644 index 0000000000..168c1e0c56 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.viewmodel + +import android.graphics.Bitmap +import com.android.quickstep.recents.usecase.GetThumbnailUseCase +import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase +import com.android.quickstep.task.thumbnail.SplashAlphaUseCase +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking + +class TaskContainerViewModel( + private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase, + private val getThumbnailUseCase: GetThumbnailUseCase, + private val splashAlphaUseCase: SplashAlphaUseCase, +) { + fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId) + + fun getSysUiStatusNavFlags(taskId: Int) = + sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId) + + fun shouldShowThumbnailSplash(taskId: Int): Boolean = + (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt new file mode 100644 index 0000000000..45b368709a --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.util.AttributeSet +import android.view.View + +class LiveTileView : View { + 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 onDraw(canvas: Canvas) { + canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT) + } + + companion object { + private val CLEAR_PAINT = + Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } + } +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt new file mode 100644 index 0000000000..7673c717a4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.graphics.Bitmap +import android.view.Surface +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.viewmodel.TaskContainerData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.recents.utilities.Utilities +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged + +class SplashAlphaUseCase( + private val recentsViewData: RecentsViewData, + private val taskContainerData: TaskContainerData, + private val taskThumbnailViewData: TaskThumbnailViewData, + private val tasksRepository: RecentTasksRepository, + private val rotationStateRepository: RecentsRotationStateRepository, +) { + fun execute(taskId: Int): Flow = + combine( + taskThumbnailViewData.width, + taskThumbnailViewData.height, + tasksRepository.getThumbnailById(taskId), + taskContainerData.thumbnailSplashProgress, + recentsViewData.thumbnailSplashProgress + ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress -> + val thumbnail = thumbnailData?.thumbnail + when { + thumbnail == null -> 0f + taskSplashProgress > 0f -> taskSplashProgress + globalSplashProgress > 0f && + isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) -> + globalSplashProgress + else -> 0f + } + } + .distinctUntilChanged() + + private fun isInaccurateThumbnail( + thumbnail: Bitmap, + thumbnailRotation: Int, + width: Int, + height: Int + ): Boolean { + return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) || + isThumbnailRotationDifferentFromTask(thumbnailRotation) + } + + private fun isThumbnailAspectRatioDifferentFromThumbnailData( + thumbnail: Bitmap, + viewWidth: Int, + viewHeight: Int + ): Boolean { + val viewAspect: Float = viewWidth / viewHeight.toFloat() + val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat() + return Utilities.isRelativePercentDifferenceGreaterThan( + viewAspect, + thumbnailAspect, + PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT + ) + } + + private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean { + val rotationState = rotationStateRepository.getRecentsRotationState() + return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) { + (rotationState.activityRotation - thumbnailRotation) % 2 != 0 + } else { + rotationState.orientationHandlerRotation != thumbnailRotation + } + } +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt new file mode 100644 index 0000000000..5fb5b90bd0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.graphics.Bitmap + +/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */ +sealed class TaskOverlayUiState { + data object Disabled : TaskOverlayUiState() + + data class Enabled(val isRealSnapshot: Boolean, val thumbnail: Bitmap?) : TaskOverlayUiState() +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt index 40f9b28eec..36a86f2526 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt @@ -17,18 +17,25 @@ package com.android.quickstep.task.thumbnail import android.graphics.Bitmap -import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Surface import androidx.annotation.ColorInt sealed class TaskThumbnailUiState { data object Uninitialized : TaskThumbnailUiState() + data object LiveTile : TaskThumbnailUiState() + data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState() + + data class SnapshotSplash( + val snapshot: Snapshot, + val splash: Drawable?, + ) : TaskThumbnailUiState() + data class Snapshot( val bitmap: Bitmap, - val drawnRect: Rect, + @Surface.Rotation val thumbnailRotation: Int, @ColorInt val backgroundColor: Int - ) : TaskThumbnailUiState() + ) } - -data class TaskThumbnail(val taskId: Int, val isRunning: Boolean) diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt index 2836c892c6..0279818b3c 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt @@ -18,82 +18,106 @@ package com.android.quickstep.task.thumbnail import android.content.Context import android.content.res.Configuration -import android.graphics.Canvas import android.graphics.Color import android.graphics.Outline -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import androidx.annotation.ColorInt +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isInvisible +import com.android.launcher3.R import com.android.launcher3.Utilities +import com.android.launcher3.util.ViewPool +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.inject 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.SnapshotSplash import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized +import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel import com.android.quickstep.util.TaskCornerRadius -import com.android.quickstep.views.RecentsView -import com.android.quickstep.views.RecentsViewContainer -import com.android.quickstep.views.TaskView +import com.android.quickstep.views.FixedSizeImageView import com.android.systemui.shared.system.QuickStepContract -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach -class TaskThumbnailView : View { - // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped - // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView] - // This is using a lazy for now because the dependencies cannot be obtained without DI. - val viewModel by lazy { - val recentsView = - RecentsViewContainer.containerFromContext(context) - .getOverviewPanel>() - TaskThumbnailViewModel( - recentsView.mRecentsViewData, - (parent as TaskView).taskViewData, - recentsView.mTasksRepository, - ) - } +class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable { + + private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this) + private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this) + + private lateinit var viewAttachedScope: CoroutineScope + + private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) } + private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) } + private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) } + private val splashBackground: View by lazy { findViewById(R.id.splash_background) } + private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) } private var uiState: TaskThumbnailUiState = Uninitialized private var inheritedScale: Float = 1f - private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val _measuredBounds = Rect() private val measuredBounds: Rect get() { _measuredBounds.set(0, 0, measuredWidth, measuredHeight) return _measuredBounds } - private var cornerRadius: Float = TaskCornerRadius.get(context) + + private var overviewCornerRadius: Float = TaskCornerRadius.get(context) private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context) - constructor(context: Context?) : super(context) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( - context: Context?, + context: Context, attrs: AttributeSet?, defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) override fun onAttachedToWindow() { super.onAttachedToWindow() - // TODO(b/335396935) replace MainScope with shorter lifecycle. - MainScope().launch { - viewModel.uiState.collect { viewModelUiState -> + viewAttachedScope = + CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView")) + viewModel.uiState + .onEach { viewModelUiState -> uiState = viewModelUiState - invalidate() + resetViews() + when (viewModelUiState) { + is Uninitialized -> {} + is LiveTile -> drawLiveWindow() + is SnapshotSplash -> drawSnapshotSplash(viewModelUiState) + is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor) + } } - } - MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } } - MainScope().launch { - viewModel.inheritedScale.collect { viewModelInheritedScale -> + .launchIn(viewAttachedScope) + viewModel.dimProgress + .onEach { dimProgress -> scrimView.alpha = dimProgress } + .launchIn(viewAttachedScope) + viewModel.splashAlpha + .onEach { splashAlpha -> + splashBackground.alpha = splashAlpha + splashIcon.alpha = splashAlpha + } + .launchIn(viewAttachedScope) + viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope) + viewModel.inheritedScale + .onEach { viewModelInheritedScale -> inheritedScale = viewModelInheritedScale invalidateOutline() } - } + .launchIn(viewAttachedScope) clipToOutline = true outlineProvider = @@ -104,46 +128,89 @@ class TaskThumbnailView : View { } } - override fun onDraw(canvas: Canvas) { - when (val uiStateVal = uiState) { - is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK) - is LiveTile -> drawTransparentUiState(canvas) - is Snapshot -> drawSnapshotState(canvas, uiStateVal) - is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor) + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + viewAttachedScope.cancel("TaskThumbnailView detaching from window") + } + + override fun onRecycle() { + uiState = Uninitialized + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + if (changed) { + viewData.width.value = abs(right - left) + viewData.height.value = abs(bottom - top) } } - private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) { - backgroundPaint.color = backgroundColor - canvas.drawRect(measuredBounds, backgroundPaint) + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (uiState is SnapshotSplash) { + setImageMatrix() + } + } + + override fun setScaleX(scaleX: Float) { + super.setScaleX(scaleX) + // Splash icon should ignore scale on TTV + splashIcon.scaleX = 1 / scaleX + } + + override fun setScaleY(scaleY: Float) { + super.setScaleY(scaleY) + // Splash icon should ignore scale on TTV + splashIcon.scaleY = 1 / scaleY } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) - cornerRadius = TaskCornerRadius.get(context) + overviewCornerRadius = TaskCornerRadius.get(context) fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context) invalidateOutline() } - private fun drawTransparentUiState(canvas: Canvas) { - canvas.drawRect(measuredBounds, CLEAR_PAINT) + private fun resetViews() { + liveTileView.isInvisible = true + thumbnailView.isInvisible = true + splashBackground.alpha = 0f + splashIcon.alpha = 0f + scrimView.alpha = 0f + setBackgroundColor(Color.BLACK) } - private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) { - drawBackgroundOnly(canvas, snapshot.backgroundColor) - canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null) + private fun drawBackground(@ColorInt background: Int) { + setBackgroundColor(background) + } + + private fun drawLiveWindow() { + liveTileView.isInvisible = false + } + + private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) { + drawSnapshot(snapshotSplash.snapshot) + + splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor) + splashIcon.setImageDrawable(snapshotSplash.splash) + } + + private fun drawSnapshot(snapshot: Snapshot) { + drawBackground(snapshot.backgroundColor) + thumbnailView.setImageBitmap(snapshot.bitmap) + thumbnailView.isInvisible = false + setImageMatrix() + } + + private fun setImageMatrix() { + thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl) } private fun getCurrentCornerRadius() = Utilities.mapRange( - viewModel.recentsFullscreenProgress.value, - cornerRadius, + viewModel.cornerRadiusProgress.value, + overviewCornerRadius, fullscreenCornerRadius ) / inheritedScale - - companion object { - private val CLEAR_PAINT = - Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } - } } diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt new file mode 100644 index 0000000000..35020294b0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.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.task.thumbnail + +import kotlinx.coroutines.flow.MutableStateFlow + +class TaskThumbnailViewData { + val width = MutableStateFlow(0) + val height = MutableStateFlow(0) +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt deleted file mode 100644 index 4511ea714c..0000000000 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt +++ /dev/null @@ -1,91 +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.annotation.ColorInt -import android.graphics.Rect -import androidx.core.graphics.ColorUtils -import com.android.quickstep.recents.data.RecentTasksRepository -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 kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map - -@OptIn(ExperimentalCoroutinesApi::class) -class TaskThumbnailViewModel( - recentsViewData: RecentsViewData, - taskViewData: TaskViewData, - private val tasksRepository: RecentTasksRepository, -) { - private val task = MutableStateFlow>(flowOf(null)) - private var boundTaskIsRunning = false - - val recentsFullscreenProgress = recentsViewData.fullscreenProgress - val inheritedScale = - combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale -> - recentsScale * taskScale - } - val uiState: Flow = - task - .flatMapLatest { taskFlow -> - taskFlow.map { taskVal -> - when { - taskVal == null -> Uninitialized - boundTaskIsRunning -> LiveTile - isBackgroundOnly(taskVal) -> - BackgroundOnly(taskVal.colorBackground.removeAlpha()) - isSnapshotState(taskVal) -> { - val bitmap = taskVal.thumbnail?.thumbnail!! - Snapshot( - bitmap, - Rect(0, 0, bitmap.width, bitmap.height), - taskVal.colorBackground.removeAlpha() - ) - } - else -> Uninitialized - } - } - } - .distinctUntilChanged() - - fun bind(taskThumbnail: TaskThumbnail) { - boundTaskIsRunning = taskThumbnail.isRunning - task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId) - } - - private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null - - private fun isSnapshotState(task: Task): Boolean { - val thumbnailPresent = task.thumbnail?.thumbnail != null - val taskLocked = task.isLocked - - return thumbnailPresent && !taskLocked - } - - @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff) -} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt new file mode 100644 index 0000000000..ab699c6013 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail.data + +import com.android.launcher3.util.CancellableTask +import com.android.quickstep.TaskIconCache.GetTaskIconCallback +import com.android.systemui.shared.recents.model.Task + +interface TaskIconDataSource { + fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>? +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt index 55598f0a2d..986acbeb31 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt @@ -22,7 +22,7 @@ import com.android.systemui.shared.recents.model.ThumbnailData import java.util.function.Consumer interface TaskThumbnailDataSource { - fun updateThumbnailInBackground( + fun getThumbnailInBackground( task: Task, callback: Consumer ): CancellableTask? diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt new file mode 100644 index 0000000000..9253dbf945 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt @@ -0,0 +1,105 @@ +/* + * 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.util + +import android.util.Log +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.task.thumbnail.TaskOverlayUiState +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled +import com.android.quickstep.task.viewmodel.TaskOverlayViewModel +import com.android.systemui.shared.recents.model.Task +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper + * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM. + */ +class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) { + private lateinit var overlayInitializedScope: CoroutineScope + private var uiState: TaskOverlayUiState = Disabled + + private val viewModel: TaskOverlayViewModel by lazy { + TaskOverlayViewModel( + task = task, + recentsViewData = RecentsDependencies.get(), + getThumbnailPositionUseCase = RecentsDependencies.get(), + recentTasksRepository = RecentsDependencies.get() + ) + } + + // TODO(b/331753115): TaskOverlay should listen for state changes and react. + val enabledState: Enabled + get() = uiState as Enabled + + fun getThumbnailMatrix() = getThumbnailPositionState().matrix + + private fun getThumbnailPositionState() = + viewModel.getThumbnailPositionState( + overlay.snapshotView.width, + overlay.snapshotView.height, + overlay.snapshotView.isLayoutRtl + ) + + fun init() { + overlayInitializedScope = + CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper")) + viewModel.overlayState + .onEach { + uiState = it + if (it is Enabled) { + initOverlay(it) + } else { + reset() + } + } + .launchIn(overlayInitializedScope) + overlay.snapshotView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + (uiState as? Enabled)?.let { initOverlay(it) } + } + } + + private fun initOverlay(enabledState: Enabled) { + Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}") + with(getThumbnailPositionState()) { + overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated) + } + } + + private fun reset() { + Log.d(TAG, "reset - taskId: ${task.key.id}") + overlay.reset() + } + + fun destroy() { + overlayInitializedScope.cancel() + uiState = Disabled + reset() + } + + companion object { + private const val TAG = "TaskOverlayHelper" + } +} diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt new file mode 100644 index 0000000000..5f2de94725 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.viewmodel + +import kotlinx.coroutines.flow.MutableStateFlow + +class TaskContainerData { + val taskMenuOpenProgress = MutableStateFlow(0f) + + val thumbnailSplashProgress = MutableStateFlow(0f) +} diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt new file mode 100644 index 0000000000..4e13d1ce26 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt @@ -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.task.viewmodel + +import android.graphics.Matrix +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled +import com.android.systemui.shared.recents.model.Task +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking + +/** View model for TaskOverlay */ +class TaskOverlayViewModel( + private val task: Task, + recentsViewData: RecentsViewData, + private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase, + recentTasksRepository: RecentTasksRepository, +) { + val overlayState = + combine( + recentsViewData.overlayEnabled, + recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) }, + recentTasksRepository.getThumbnailById(task.key.id) + ) { isOverlayEnabled, isFullyVisible, thumbnailData -> + if (isOverlayEnabled && isFullyVisible) { + Enabled( + isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked, + thumbnailData?.thumbnail, + ) + } else { + Disabled + } + } + .distinctUntilChanged() + + fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState { + return runBlocking { + val matrix: Matrix + val isRotated: Boolean + when ( + val thumbnailPositionState = + getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl) + ) { + is MatrixScaling -> { + matrix = thumbnailPositionState.matrix + isRotated = thumbnailPositionState.isRotated + } + is MissingThumbnail -> { + matrix = Matrix.IDENTITY_MATRIX + isRotated = false + } + } + ThumbnailPositionState(matrix, isRotated) + } + } + + data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean) +} diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt new file mode 100644 index 0000000000..b1bb65e85e --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.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 goveryning permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.viewmodel + +import android.annotation.ColorInt +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.graphics.Matrix +import androidx.core.graphics.ColorUtils +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.usecase.ThumbnailPositionState +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.thumbnail.SplashAlphaUseCase +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState +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.SnapshotSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized +import com.android.systemui.shared.recents.model.Task +import kotlin.math.max +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking + +@OptIn(ExperimentalCoroutinesApi::class) +class TaskThumbnailViewModel( + recentsViewData: RecentsViewData, + taskViewData: TaskViewData, + taskContainerData: TaskContainerData, + private val tasksRepository: RecentTasksRepository, + private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase, + private val splashAlphaUseCase: SplashAlphaUseCase, +) { + private val task = MutableStateFlow>(flowOf(null)) + private val splashProgress = MutableStateFlow(flowOf(0f)) + private var taskId: Int = INVALID_TASK_ID + + /** + * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen + * corner radius. + */ + val cornerRadiusProgress = + if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress + else MutableStateFlow(1f).asStateFlow() + + val inheritedScale = + combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale -> + recentsScale * taskScale + } + + val dimProgress: Flow = + combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) { + taskMenuOpenProgress, + tintAmount -> + max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount) + } + val splashAlpha = splashProgress.flatMapLatest { it } + + private val isLiveTile = + combine( + task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(), + recentsViewData.runningTaskIds, + recentsViewData.runningTaskShowScreenshot + ) { taskId, runningTaskIds, runningTaskShowScreenshot -> + runningTaskIds.contains(taskId) && !runningTaskShowScreenshot + } + .distinctUntilChanged() + + val uiState: Flow = + combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning -> + when { + taskVal == null -> Uninitialized + isRunning -> LiveTile + isBackgroundOnly(taskVal) -> + BackgroundOnly(taskVal.colorBackground.removeAlpha()) + isSnapshotSplashState(taskVal) -> + SnapshotSplash(createSnapshotState(taskVal), taskVal.icon) + else -> Uninitialized + } + } + .distinctUntilChanged() + + fun bind(taskId: Int) { + this.taskId = taskId + task.value = tasksRepository.getTaskDataById(taskId) + splashProgress.value = splashAlphaUseCase.execute(taskId) + } + + fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix { + return runBlocking { + when ( + val thumbnailPositionState = + getThumbnailPositionUseCase.run(taskId, width, height, isRtl) + ) { + is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix + is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX + } + } + } + + private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null + + private fun isSnapshotSplashState(task: Task): Boolean { + val thumbnailPresent = task.thumbnail?.thumbnail != null + val taskLocked = task.isLocked + + return thumbnailPresent && !taskLocked + } + + private fun createSnapshotState(task: Task): Snapshot { + val thumbnailData = task.thumbnail + val bitmap = thumbnailData?.thumbnail!! + return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha()) + } + + @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff) + + private companion object { + const val MAX_SCRIM_ALPHA = 0.4f + } +} diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt index a8b5112860..7a9ecf2426 100644 --- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt +++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt @@ -16,9 +16,14 @@ package com.android.quickstep.task.viewmodel +import com.android.quickstep.views.TaskViewType import kotlinx.coroutines.flow.MutableStateFlow -class TaskViewData { +class TaskViewData(taskViewType: TaskViewType) { // This is typically a View concern but it is used to invalidate rendering in other Views val scale = MutableStateFlow(1f) + + // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM. + /** Whether outline of TaskView is formed by outline thumbnail view(s). */ + val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP } diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt new file mode 100644 index 0000000000..ec75d59c34 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.viewmodel + +import androidx.lifecycle.ViewModel + +class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() { + fun updateScale(scale: Float) { + taskViewData.scale.value = scale + } +} diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java index cfa6b9891f..2398e6627e 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java +++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java @@ -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/com/android/quickstep/util/ActiveGestureLog.java index c54862aba3..d46b8fc51a 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java +++ b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java @@ -23,6 +23,7 @@ import com.android.launcher3.util.Preconditions; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -237,7 +238,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; diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java index 8e3d44f8d8..31aca03ce6 100644 --- a/quickstep/src/com/android/quickstep/util/AnimUtils.java +++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java @@ -17,18 +17,28 @@ package com.android.quickstep.util; import static com.android.app.animation.Interpolators.clampToProgress; +import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import android.animation.AnimatorSet; +import android.annotation.NonNull; import android.os.Bundle; import android.os.IRemoteCallback; import android.view.animation.Interpolator; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.util.RunnableList; +import com.android.quickstep.views.RecentsViewContainer; /** * Utility class containing methods to help manage animations, interpolators, and timings. */ public class AnimUtils { + private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350; + /** * Fetches device-specific timings for the Overview > Split animation * (splitscreen initiated from Overview). @@ -58,6 +68,33 @@ public class AnimUtils { : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH; } + /** + * Synchronizes the timing for the split dismiss animation to the current transition to + * NORMAL (launcher home/workspace) + */ + public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager, + @NonNull RecentsViewContainer container, + @NonNull StatsLogManager.LauncherEvent exitReason, + @NonNull SplitAnimationController animationController) { + StateAnimationConfig config = new StateAnimationConfig(); + BaseState startState = stateManager.getState(); + long duration = startState.getTransitionDuration(container.asContext(), + false /*isToState*/); + if (duration == 0) { + // Case where we're in contextual on workspace (NORMAL), which by default has 0 + // transition duration + duration = DURATION_DEFAULT_SPLIT_DISMISS; + } + config.duration = duration; + AnimatorSet stateAnim = stateManager.createAtomicAnimation( + startState, NORMAL, config); + AnimatorSet dismissAnim = animationController + .createPlaceholderDismissAnim(container, exitReason, duration); + stateAnim.play(dismissAnim); + stateManager.setCurrentAnimation(stateAnim, NORMAL); + stateAnim.start(); + } + /** * Returns a IRemoteCallback which completes the provided list as a result */ diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java index c7c04ed91c..b583a4bd3f 100644 --- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java +++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java @@ -21,7 +21,6 @@ import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; -import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.content.Context; import android.graphics.Matrix; @@ -34,18 +33,11 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.statemanager.StateManager; -import com.android.launcher3.statemanager.StatefulActivity; -import com.android.launcher3.states.StateAnimationConfig; -import com.android.launcher3.touch.AllAppsSwipeController; -import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; import com.android.quickstep.views.RecentsView; -import com.android.quickstep.views.RecentsViewContainer; /** * Controls an animation that can go beyond progress = 1, at which point resistance should be @@ -57,10 +49,8 @@ public class AnimatorControllerWithResistance { private enum RecentsResistanceParams { FROM_APP(0.75f, 0.5f, 1f, false), - FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false), FROM_APP_TABLET(1f, 0.7f, 1f, true), FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true), - FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false), FROM_OVERVIEW(1f, 0.75f, 0.5f, false); RecentsResistanceParams(float scaleStartResist, float scaleMaxResist, @@ -157,46 +147,10 @@ public class AnimatorControllerWithResistance { RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget, scaleProperty, translationTarget, translationProperty); PendingAnimation resistAnim = createRecentsResistanceAnim(params); - - // Apply All Apps animation during the resistance animation. - if (recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()) { - RecentsViewContainer container = - recentsOrientedState.getContainerInterface().getCreatedContainer(); - if (container != null) { - RecentsView recentsView = container.getOverviewPanel(); - StateManager> stateManager = - recentsView.getStateManager(); - if (stateManager.isInStableState(LauncherState.BACKGROUND_APP) - && stateManager.isInTransition()) { - - // Calculate the resistance progress threshold where All Apps will trigger. - float threshold = getAllAppsThreshold(context, recentsOrientedState, dp); - - StateAnimationConfig config = new StateAnimationConfig(); - AllAppsSwipeController.applyOverviewToAllAppsAnimConfig(dp, config, threshold); - AnimatorSet allAppsAnimator = stateManager.createAnimationToNewWorkspace( - LauncherState.ALL_APPS, config).getTarget(); - resistAnim.add(allAppsAnimator); - } - } - } - AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController(); return new AnimatorControllerWithResistance(normalController, resistanceController); } - private static float getAllAppsThreshold(Context context, - RecentsOrientedState recentsOrientedState, DeviceProfile dp) { - int transitionDragLength = - recentsOrientedState.getContainerInterface().getSwipeUpDestinationAndLength( - dp, context, TEMP_RECT, - recentsOrientedState.getOrientationHandler()); - float dragLengthFactor = (float) dp.heightPx / transitionDragLength; - // -1s are because 0-1 is reserved for the normal transition. - float threshold = DeviceConfigWrapper.get().getAllAppsOverviewThreshold() / 100f; - return (threshold - 1) / (dragLengthFactor - 1); - } - /** * Creates the resistance animation for {@link #createForRecents}, or can be used separately * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}. @@ -305,17 +259,11 @@ public class AnimatorControllerWithResistance { this.translationTarget = translationTarget; this.translationProperty = translationProperty; if (dp.isTablet) { - resistanceParams = - recentsOrientedState.getContainerInterface().allowAllAppsFromOverview() - ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET - : enableGridOnlyOverview() - ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY - : RecentsResistanceParams.FROM_APP_TABLET; + resistanceParams = enableGridOnlyOverview() + ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY + : RecentsResistanceParams.FROM_APP_TABLET; } else { - resistanceParams = - recentsOrientedState.getContainerInterface().allowAllAppsFromOverview() - ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS - : RecentsResistanceParams.FROM_APP; + resistanceParams = RecentsResistanceParams.FROM_APP; } } diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index 6f9cbfd74a..e1013dbe36 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -22,16 +22,15 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH; import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_NONE; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.isPersistentSnapPosition; import android.content.Context; import android.content.Intent; @@ -45,8 +44,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.jank.Cuj; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.R; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.apppairs.AppPairIcon; @@ -69,13 +66,15 @@ import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.views.GroupedTaskView; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; /** * Controller class that handles app pair interactions: saving, modifying, deleting, etc. @@ -135,7 +134,7 @@ public class AppPairsController { } GroupedTaskView gtv = (GroupedTaskView) taskView; - List containers = gtv.getTaskContainers(); + List containers = gtv.getTaskContainers(); ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask( containers.get(0).getTask().key); ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask( @@ -172,7 +171,7 @@ public class AppPairsController { */ public void saveAppPair(GroupedTaskView gtv) { InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); - List containers = gtv.getTaskContainers(); + List containers = gtv.getTaskContainers(); WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo(); WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo(); WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1); @@ -234,14 +233,16 @@ public class AppPairsController { * * @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank * monitoring + * @param callback Called after the app pair launch finishes animating, or null if no method is + * to be called */ - public void launchAppPair(AppPairIcon appPairIcon, int cuj) { + public void launchAppPair(AppPairIcon appPairIcon, int cuj, + @Nullable Consumer callback) { WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp(); WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp(); ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user); ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user); - mSplitSelectStateController.setLaunchingCuj(cuj); - InteractionJankMonitorWrapper.begin(appPairIcon, cuj); + mSplitSelectStateController.setLaunchingCuj(appPairIcon, cuj); mSplitSelectStateController.findLastActiveTasksAndRunCallback( Arrays.asList(app1Key, app2Key), @@ -275,11 +276,18 @@ public class AppPairsController { mSplitSelectStateController.setLaunchingIconView(appPairIcon); mSplitSelectStateController.launchSplitTasks( - AppPairsController.convertRankToSnapPosition(app1.rank)); + AppPairsController.convertRankToSnapPosition(app1.rank), callback); } ); } + /** + * Launches an app pair but does not specify a callback + */ + public void launchAppPair(AppPairIcon appPairIcon, int cuj) { + launchAppPair(appPairIcon, cuj, null); + } + /** * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such * package exists in the AllAppsStore. diff --git a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java index 0ce54ad2e6..2e3dee6e09 100644 --- a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java +++ b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java @@ -81,7 +81,7 @@ public class AssistContentRequester { try { mActivityTaskManager.requestAssistDataForTask( new AssistDataReceiver(callback, this), taskId, mPackageName, - mAttributionTag); + mAttributionTag, false /* fetchStructure */); } catch (RemoteException e) { Log.e(TAG, "Requesting assist content failed for task: " + taskId, e); } diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java index cda87c0f36..38ae3039d8 100644 --- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java +++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java @@ -52,7 +52,7 @@ public class AsyncClockEventDelegate extends ClockEventDelegate private final Context mContext; private final SimpleBroadcastReceiver mReceiver = - new SimpleBroadcastReceiver(this::onClockEventReceived); + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived); private final ArrayMap mTimeEventReceivers = new ArrayMap<>(); private final List mFormatObservers = new ArrayList<>(); @@ -64,9 +64,7 @@ public class AsyncClockEventDelegate extends ClockEventDelegate private AsyncClockEventDelegate(Context context) { super(context); mContext = context; - - UI_HELPER_EXECUTOR.execute(() -> - mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED)); + mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED); } @Override @@ -127,6 +125,6 @@ public class AsyncClockEventDelegate extends ClockEventDelegate public void close() { mDestroyed = true; SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this); - UI_HELPER_EXECUTOR.execute(() -> mReceiver.unregisterReceiverSafely(mContext)); + mReceiver.unregisterReceiverSafely(mContext); } } diff --git a/quickstep/src/com/android/quickstep/util/BackAnimState.kt b/quickstep/src/com/android/quickstep/util/BackAnimState.kt new file mode 100644 index 0000000000..9009eaa263 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/BackAnimState.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.util + +import android.animation.AnimatorSet +import android.content.Context +import com.android.launcher3.LauncherAnimationRunner.AnimationResult +import com.android.launcher3.anim.AnimatorListeners.forEndCallback +import com.android.launcher3.util.RunnableList + +/** Interface to represent animation for back to Launcher transition */ +interface BackAnimState { + + fun addOnAnimCompleteCallback(r: Runnable) + + fun applyToAnimationResult(result: AnimationResult, c: Context) + + fun start() +} + +class AnimatorBackState(private val springAnim: RectFSpringAnim?, private val anim: AnimatorSet?) : + BackAnimState { + + override fun addOnAnimCompleteCallback(r: Runnable) { + val springAnimWait = RunnableList() + springAnim?.addAnimatorListener(forEndCallback(springAnimWait::executeAllAndDestroy)) + ?: springAnimWait.executeAllAndDestroy() + + val animWait = RunnableList() + anim?.addListener( + forEndCallback(Runnable { springAnimWait.add(animWait::executeAllAndDestroy) }) + ) ?: springAnimWait.add(animWait::executeAllAndDestroy) + animWait.add(r) + } + + override fun applyToAnimationResult(result: AnimationResult, c: Context) { + result.setAnimation(anim, c) + } + + override fun start() { + anim?.start() + } +} + +class AlreadyStartedBackAnimState(private val onEndCallback: RunnableList) : BackAnimState { + + override fun addOnAnimCompleteCallback(r: Runnable) { + onEndCallback.add(r) + } + + override fun applyToAnimationResult(result: AnimationResult, c: Context) { + addOnAnimCompleteCallback(result::onAnimationFinished) + } + + override fun start() {} +} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java index 8d99069c19..a727aa2efb 100644 --- a/quickstep/src/com/android/quickstep/util/DesktopTask.java +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java @@ -18,10 +18,11 @@ package com.android.quickstep.util; import androidx.annotation.NonNull; -import com.android.quickstep.views.TaskView; +import com.android.quickstep.views.TaskViewType; import com.android.systemui.shared.recents.model.Task; import java.util.List; +import java.util.Objects; /** * A {@link Task} container that can contain N number of tasks that are part of the desktop in @@ -33,7 +34,7 @@ public class DesktopTask extends GroupTask { public final List tasks; public DesktopTask(@NonNull List tasks) { - super(tasks.get(0), null, null, TaskView.Type.DESKTOP); + super(tasks.get(0), null, null, TaskViewType.DESKTOP); this.tasks = tasks; } @@ -68,4 +69,16 @@ public class DesktopTask extends GroupTask { return "type=" + taskViewType + " tasks=" + tasks; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DesktopTask that)) return false; + if (!super.equals(o)) return false; + return Objects.equals(tasks, that.tasks); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), tasks); + } } diff --git a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt index d36dc7e754..2dd727e240 100644 --- a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt +++ b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt @@ -26,19 +26,21 @@ import android.provider.DeviceConfig.Properties import androidx.annotation.WorkerThread import com.android.launcher3.BuildConfig import com.android.launcher3.util.Executors +import java.util.concurrent.CopyOnWriteArrayList /** Utility class to manage a set of device configurations */ class DeviceConfigHelper(private val factory: (PropReader) -> ConfigType) { var config: ConfigType private set + private val allKeys: Set private val propertiesListener = OnPropertiesChangedListener { onDevicePropsChanges(it) } private val sharedPrefChangeListener = OnSharedPreferenceChangeListener { _, _ -> recreateConfig() } - private val changeListeners = mutableListOf() + private val changeListeners = CopyOnWriteArrayList() init { // Initialize the default config once. diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java index 945ffe31f6..fba08a96b2 100644 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ b/quickstep/src/com/android/quickstep/util/GroupTask.java @@ -20,12 +20,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; -import com.android.quickstep.views.TaskView; +import com.android.quickstep.views.TaskViewType; import com.android.systemui.shared.recents.model.Task; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * A {@link Task} container that can contain one or two tasks, depending on if the two tasks @@ -38,19 +39,18 @@ public class GroupTask { public final Task task2; @Nullable public final SplitBounds mSplitBounds; - @TaskView.Type - public final int taskViewType; + public final TaskViewType taskViewType; public GroupTask(@NonNull Task task) { this(task, null, null); } public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) { - this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE); + this(t1, t2, splitBounds, t2 != null ? TaskViewType.GROUPED : TaskViewType.SINGLE); } protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds, - @TaskView.Type int taskViewType) { + TaskViewType taskViewType) { task1 = t1; task2 = t2; mSplitBounds = splitBounds; @@ -91,4 +91,17 @@ public class GroupTask { return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GroupTask that)) return false; + return taskViewType == that.taskViewType && Objects.equals(task1, + that.task1) && Objects.equals(task2, that.task2) + && Objects.equals(mSplitBounds, that.mSplitBounds); + } + + @Override + public int hashCode() { + return Objects.hash(task1, task2, mSplitBounds, taskViewType); + } } diff --git a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java index cb44a1a0d8..fcf9ab1ad4 100644 --- a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java +++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java @@ -108,19 +108,28 @@ public class InputConsumerProxy { return false; } + final SimpleOrientationTouchTransformer touchTransformer = + SimpleOrientationTouchTransformer.INSTANCE.get(mContext); + final int viewRotation = mRotationSupplier.get(); + final boolean needTransform = viewRotation != ev.getSurfaceRotation(); if (action == ACTION_DOWN) { mTouchInProgress = true; + if (needTransform) { + touchTransformer.updateTouchingOrientation(viewRotation); + } initInputConsumerIfNeeded(/* isFromTouchDown= */ true); } else if (action == ACTION_CANCEL || action == ACTION_UP) { // Finish any pending actions mTouchInProgress = false; + touchTransformer.clearTouchingOrientation(); if (mDestroyPending) { destroy(); } } if (mInputConsumer != null) { - SimpleOrientationTouchTransformer.INSTANCE.get(mContext).transform(ev, - mRotationSupplier.get()); + if (needTransform) { + touchTransformer.transform(ev, viewRotation); + } mInputConsumer.onMotionEvent(ev); } diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java index 26668c8da3..4c26761440 100644 --- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java +++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java @@ -31,7 +31,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Hotseat; import com.android.launcher3.Workspace; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.HorizontalInsettableView; import com.android.quickstep.SystemUiProxy; @@ -80,17 +79,12 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL @UnfoldMain RotationChangeProvider rotationChangeProvider) { mLauncher = launcher; - if (FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider( - unfoldTransitionProgressProvider, launcher.getMainThreadHandler()); - mPreemptiveProgressProvider.init(); + mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider( + unfoldTransitionProgressProvider, launcher.getMainThreadHandler()); + mPreemptiveProgressProvider.init(); - mProgressProvider = new ScopedUnfoldTransitionProgressProvider( - mPreemptiveProgressProvider); - } else { - mProgressProvider = new ScopedUnfoldTransitionProgressProvider( - unfoldTransitionProgressProvider); - } + mProgressProvider = new ScopedUnfoldTransitionProgressProvider( + mPreemptiveProgressProvider); unfoldTransitionProgressProvider.addCallback(mExternalTransitionStatusProvider); unfoldTransitionProgressProvider.addCallback( @@ -169,10 +163,6 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL @Override public void onDeviceProfileChanged(DeviceProfile dp) { - if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - return; - } - if (mIsTablet != null && dp.isTablet != mIsTablet) { // We should preemptively start the animation only if: // - We changed to the unfolded screen diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java index ec1eeb1d6b..a8460c9d7c 100644 --- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java +++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java @@ -23,11 +23,13 @@ import android.view.ViewGroup; import com.android.launcher3.DeviceProfile; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.NavigationMode; -import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; public class LayoutUtils { + private static final float SQUARE_ASPECT_RATIO_TOLERANCE = 0.05f; + /** * The height for the swipe up motion */ @@ -39,11 +41,14 @@ public class LayoutUtils { return swipeHeight; } - public static int getShelfTrackingDistance(Context context, DeviceProfile dp, - RecentsPagedOrientationHandler orientationHandler) { + public static int getShelfTrackingDistance( + Context context, + DeviceProfile dp, + RecentsPagedOrientationHandler orientationHandler, + BaseContainerInterface baseContainerInterface) { // Track the bottom of the window. Rect taskSize = new Rect(); - LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize, + baseContainerInterface.calculateTaskSize(context, dp, taskSize, orientationHandler); return orientationHandler.getDistanceToBottomOfRect(dp, taskSize); } @@ -61,4 +66,13 @@ public class LayoutUtils { } } } + + /** + * Returns true iff the device's aspect ratio is within + * {@link LayoutUtils#SQUARE_ASPECT_RATIO_TOLERANCE} of 1:1 + */ + public static boolean isAspectRatioSquare(float aspectRatio) { + return Float.compare(aspectRatio, 1f - SQUARE_ASPECT_RATIO_TOLERANCE) >= 0 + && Float.compare(aspectRatio, 1f + SQUARE_ASPECT_RATIO_TOLERANCE) <= 0; + } } diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java index b8bc828ec3..15081da2de 100644 --- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java +++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java @@ -66,6 +66,7 @@ public class MotionPauseDetector { private Float mPreviousVelocity = null; private OnMotionPauseListener mOnMotionPauseListener; + private boolean mIsTrackpadGesture; private boolean mIsPaused; // Bias more for the first pause to make it feel extra responsive. private boolean mHasEverBeenPaused; @@ -115,6 +116,10 @@ public class MotionPauseDetector { mOnMotionPauseListener = listener; } + public void setIsTrackpadGesture(boolean isTrackpadGesture) { + mIsTrackpadGesture = isTrackpadGesture; + } + /** * @param disallowPause If true, we will not detect any pauses until this is set to false again. */ @@ -179,7 +184,8 @@ public class MotionPauseDetector { // We want to be more aggressive about detecting the first pause to ensure it // feels as responsive as possible; getting two very slow speeds back to back // takes too long, so also check for a rapid deceleration. - boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR; + boolean isRapidDeceleration = + speed < previousSpeed * getRapidDecelerationFactor(); isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; isPausedReason = new ActiveGestureLog.CompoundString( "Didn't have back to back slow speeds, checking for rapid ") @@ -253,6 +259,7 @@ public class MotionPauseDetector { mVelocityProvider.clear(); mPreviousVelocity = null; setOnMotionPauseListener(null); + mIsTrackpadGesture = false; mIsPaused = mHasEverBeenPaused = false; mSlowStartTime = 0; mForcePauseTimeout.cancelAlarm(); @@ -262,6 +269,12 @@ public class MotionPauseDetector { return mIsPaused; } + private float getRapidDecelerationFactor() { + return mIsTrackpadGesture ? Float.parseFloat( + Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor", + String.valueOf(RAPID_DECELERATION_FACTOR))) : RAPID_DECELERATION_FACTOR; + } + public interface OnMotionPauseListener { /** Called only the first time motion pause is detected. */ void onMotionPauseDetected(); diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java index cfe5b2cd61..70ef47cc77 100644 --- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java +++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java @@ -106,7 +106,8 @@ public class QuickstepOnboardingPrefs { return; } mShouldIncreaseCount = toState == HINT_STATE - && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE; + && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE + && launcher.isCanShowAllAppsEducationView(); } @Override diff --git a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java index 0b05c2e7c1..63fe017023 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java +++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; +import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA; import android.animation.Animator; import android.animation.ObjectAnimator; @@ -33,8 +34,10 @@ public class RecentsAtomicAnimationFactory = + 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 { + val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP } + return otherTasks + desktopTasks + } + + /** Returns the expected index of the focus task. */ + fun getFocusedTaskIndex(taskGroups: List): Int { + // The focused task index is placed after the desktop tasks views. + return if (enableLargeDesktopWindowingTile()) { + taskGroups.count { it.taskViewType == TaskViewType.DESKTOP } + } else { + 0 + } + } + + /** Counts [TaskView]s that are [DesktopTaskView] instances. */ + fun getDesktopTaskViewCount(taskViews: Iterable): Int = + taskViews.count { it is DesktopTaskView } + + /** Returns a list of all large TaskView Ids from [TaskView]s */ + fun getLargeTaskViewIds(taskViews: Iterable): List = + taskViews.filter { it.isLargeTile }.map { it.taskViewId } + + /** + * Returns the first TaskView that should be displayed as a large tile. + * + * @param taskViews List of [TaskView]s + */ + fun getFirstLargeTaskView(taskViews: Iterable): TaskView? = + taskViews.firstOrNull { it.isLargeTile } + + /** Returns the last TaskView that should be displayed as a large tile. */ + fun getLastLargeTaskView(taskViews: Iterable): TaskView? = + taskViews.lastOrNull { it.isLargeTile } + + /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */ + fun getFirstTaskViewInCarousel( + nonRunningTaskCategoryHidden: Boolean, + taskViews: Iterable, + runningTaskView: TaskView?, + ): TaskView? = + taskViews.firstOrNull { + it.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden) + } + + /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */ + fun getLastTaskViewInCarousel( + nonRunningTaskCategoryHidden: Boolean, + taskViews: Iterable, + runningTaskView: TaskView?, + ): TaskView? = + taskViews.lastOrNull { + it.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden) + } + + /** Returns the current list of [TaskView] children. */ + fun getTaskViews(taskViewCount: Int, requireTaskViewAt: (Int) -> TaskView): Iterable = + (0 until taskViewCount).map(requireTaskViewAt) + + /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */ + fun applyAttachAlpha( + taskViews: Iterable, + runningTaskView: TaskView?, + runningTaskTileHidden: Boolean, + nonRunningTaskCategoryHidden: Boolean, + ) { + taskViews.forEach { taskView -> + val isVisible = + if (taskView == runningTaskView) !runningTaskTileHidden + else taskView.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden) + taskView.attachAlpha = if (isVisible) 1f else 0f + } + } + + private fun TaskView.isVisibleInCarousel( + runningTaskView: TaskView?, + nonRunningTaskCategoryHidden: Boolean, + ): Boolean = + if (!nonRunningTaskCategoryHidden) true + else if (runningTaskView == null) true else getCategory() == runningTaskView.getCategory() + + private fun TaskView.getCategory(): TaskViewCategory = + if (this is DesktopTaskView) TaskViewCategory.DESKTOP else TaskViewCategory.FULL_SCREEN + + private enum class TaskViewCategory { + FULL_SCREEN, + DESKTOP, + } +} diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt index dbeedd33f7..ece958395f 100644 --- a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt +++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt @@ -36,6 +36,8 @@ class SlideInRemoteTransition( private val pageSpacing: Int, private val cornerRadius: Float, private val interpolator: TimeInterpolator, + private val onStartCallback: Runnable, + private val onFinishCallback: Runnable, ) : RemoteTransitionStub() { private val animationDurationMs = 500L @@ -68,6 +70,7 @@ class SlideInRemoteTransition( startT.setCrop(leash, chg.endAbsBounds).setCornerRadius(leash, cornerRadius) } } + onStartCallback.run() startT.apply() anim.addUpdateListener { @@ -97,6 +100,7 @@ class SlideInRemoteTransition( val t = Transaction() try { finishCB.onTransitionFinished(null, t) + onFinishCallback.run() } catch (e: RemoteException) { // Ignore } diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 7ea04b12a7..3449cf2034 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -27,8 +27,10 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.graphics.Bitmap +import android.graphics.Color import android.graphics.Rect import android.graphics.RectF +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.view.RemoteAnimationTarget import android.view.SurfaceControl @@ -40,9 +42,12 @@ import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken import androidx.annotation.VisibleForTesting +import androidx.core.util.component1 +import androidx.core.util.component2 import com.android.app.animation.Interpolators import com.android.launcher3.DeviceProfile import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.InsettableFrameLayout import com.android.launcher3.QuickstepTransitionManager import com.android.launcher3.R @@ -67,9 +72,9 @@ import com.android.quickstep.views.IconAppChipView import com.android.quickstep.views.RecentsView import com.android.quickstep.views.RecentsViewContainer import com.android.quickstep.views.SplitInstructionsView +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView -import com.android.quickstep.views.TaskView.TaskContainer import com.android.quickstep.views.TaskViewIcon import com.android.wm.shell.shared.TransitionUtil import java.util.Optional @@ -88,7 +93,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val iconDrawable: Drawable, val fadeWithThumbnail: Boolean, val isStagedTask: Boolean, - val iconView: View? + val iconView: View?, + val contentDescription: CharSequence? ) } @@ -109,7 +115,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, - iconView = null + iconView = null, + splitSelectSource.itemInfo.contentDescription ) } else if (splitSelectStateController.isDismissingFromSplitPair) { // Initiating split from overview, but on a split pair @@ -118,12 +125,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { val drawable = getDrawable(container.iconView, splitSelectSource) return SplitAnimInitProps( - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, - drawable!!, + container.snapshotView, + container.splitAnimationThumbnail, + drawable, fadeWithThumbnail = true, isStagedTask = true, - iconView = container.iconView.asView() + iconView = container.iconView.asView(), + container.task.titleDescription ) } } @@ -137,12 +145,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC taskView.taskContainers.first().let { val drawable = getDrawable(it.iconView, splitSelectSource) return SplitAnimInitProps( - it.thumbnailViewDeprecated, - it.thumbnailViewDeprecated.thumbnail, - drawable!!, + it.snapshotView, + it.splitAnimationThumbnail, + drawable, fadeWithThumbnail = true, isStagedTask = true, - iconView = it.iconView.asView() + iconView = it.iconView.asView(), + it.task.titleDescription ) } } @@ -151,41 +160,59 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC /** * Returns the drawable that's provided in iconView, however if that is null it falls back to * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the - * TaskView is scrolled far enough off screen + * TaskView is scrolled far enough off screen. * - * @return [Drawable] + * @return the [Drawable] icon, or a translucent drawable if none was found */ - fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? { - if (iconView.drawable == null && splitSelectSource != null) { - return splitSelectSource.drawable - } - return iconView.drawable + fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable { + val drawable = + if (iconView.drawable == null && splitSelectSource != null) splitSelectSource.drawable + else iconView.drawable + return drawable ?: ColorDrawable(Color.TRANSPARENT) } /** * When selecting first app from split pair, second app's thumbnail remains. This animates the * second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it - * with [TaskThumbnailViewDeprecated]'s splashView. Adds animations to the provided builder. - * Note: The app that **was not** selected as the first split app should be the container that's - * passed through. + * with [TaskContainer]'s splashView. Adds animations to the provided builder. Note: The app + * that **was not** selected as the first split app should be the container that's passed + * through. * * @param builder Adds animation to this - * @param taskIdAttributeContainer container of the app that **was not** selected + * @param taskContainer container of the app that **was not** selected * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair - * (opposite of that representing [taskIdAttributeContainer]) + * (opposite of that representing [taskContainer]) */ fun addInitialSplitFromPair( - taskIdAttributeContainer: TaskContainer, + taskContainer: TaskContainer, builder: PendingAnimation, deviceProfile: DeviceProfile, taskViewWidth: Int, taskViewHeight: Int, isPrimaryTaskSplitting: Boolean ) { - val thumbnail = taskIdAttributeContainer.thumbnailViewDeprecated - val iconView: View = taskIdAttributeContainer.iconView.asView() - builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailViewDeprecated.SPLASH_ALPHA, 1f)) - thumbnail.setShowSplashForSplitSelection(true) + val snapshot = taskContainer.snapshotView + val iconView: View = taskContainer.iconView.asView() + if (!enableRefactorTaskThumbnail()) { + val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated + builder.add( + ObjectAnimator.ofFloat( + thumbnailViewDeprecated, + TaskThumbnailViewDeprecated.SPLASH_ALPHA, + 1f + ) + ) + thumbnailViewDeprecated.setShowSplashForSplitSelection(true) + } else { + builder.add( + ValueAnimator.ofFloat(0f, 1f).apply { + addUpdateListener { + taskContainer.taskContainerData.thumbnailSplashProgress.value = + it.animatedFraction + } + } + ) + } // With the new `IconAppChipView`, we always want to keep the chip pinned to the // top left of the task / thumbnail. if (enableOverviewIconMenu()) { @@ -200,16 +227,24 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f) ) } + + val splitBoundsConfig = + (taskContainer.taskView as? GroupedTaskView)?.splitBoundsConfig ?: return + val (primarySnapshotViewSize, secondarySnapshotViewSize) = + taskContainer.taskView.pagedOrientationHandler.getGroupedTaskViewSizes( + deviceProfile, + splitBoundsConfig, + taskViewWidth, + taskViewHeight, + ) + val snapshotViewSize = + if (isPrimaryTaskSplitting) primarySnapshotViewSize else secondarySnapshotViewSize if (deviceProfile.isLeftRightSplit) { // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 - val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f - val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width + val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f + val finalScaleX: Float = taskViewWidth.toFloat() / snapshotViewSize.x builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - centerThumbnailTranslationX - ) + ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, centerThumbnailTranslationX) ) if (!enableOverviewIconMenu()) { // icons are anchored from Gravity.END, so need to use negative translation @@ -218,21 +253,15 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX) ) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) + builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_X, finalScaleX)) // Reset other dimensions // TODO(b/271468547), can't set Y translate to 0, need to account for top space - thumbnail.scaleY = 1f + snapshot.scaleY = 1f val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() - builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - translateYResetVal - ) - ) + builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, translateYResetVal)) } else { val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 @@ -247,36 +276,26 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // thumbnail needs to take that into account. We should migrate to only using // translations otherwise this asymmetry causes problems.. if (isPrimaryTaskSplitting) { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() } else { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f } - val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height + val finalScaleY: Float = thumbnailSize.toFloat() / snapshotViewSize.y builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - centerThumbnailTranslationY - ) + ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, centerThumbnailTranslationY) ) if (!enableOverviewIconMenu()) { // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f)) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) + builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_Y, finalScaleY)) // Reset other dimensions - thumbnail.scaleX = 1f - builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - 0f - ) - ) + snapshot.scaleX = 1f + builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, 0f)) } } @@ -495,7 +514,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC depthController: DepthController?, info: TransitionInfo?, t: Transaction?, - finishCallback: Runnable + finishCallback: Runnable, + cornerRadius: Float ) { if (info == null && t == null) { // (Legacy animation) Tapping a split tile in Overview @@ -542,7 +562,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info) if (appPairLaunchingAppIndex == -1) { // Launch split app pair animation - composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + composeIconSplitLaunchAnimator( + launchingIconView, + info, + t, + finishCallback, + cornerRadius + ) } else { composeFullscreenIconSplitLaunchAnimator( launchingIconView, @@ -559,7 +585,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC "unexpected null" } - composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback) + composeFadeInSplitLaunchAnimator( + initialTaskId, + secondTaskId, + info, + t, + finishCallback, + cornerRadius + ) } } @@ -659,15 +692,15 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC * To find the root shell leash that we want to fade in, we do the following: The Changes we * receive in transitionInfo are structured like this * - * Root (grandparent) + * (0) Root (grandparent) * | - * |--> 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 +715,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 @@ -702,48 +736,29 @@ 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 @@ -762,9 +777,26 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ) 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() } @@ -1037,7 +1069,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 @@ -1115,6 +1148,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/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt index 38bbe601b6..d982e81c42 100644 --- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt @@ -16,11 +16,20 @@ 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 @@ -31,28 +40,58 @@ class SplitScreenUtils { null } else { SplitConfigurationOptions.SplitBounds( - shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds, - shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId, + 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 7e7c79430d..1af12f1ac1 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_50_50; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -56,13 +55,11 @@ 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; @@ -94,17 +91,15 @@ 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; @@ -154,9 +149,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; @@ -275,17 +271,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; } } @@ -459,77 +453,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 +533,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,34 +565,16 @@ 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_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); } } @@ -659,7 +592,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 +600,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 +652,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; } @@ -765,6 +695,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, @@ -778,7 +711,8 @@ public class SplitSelectStateController { info, t, () -> { finishAdapter.run(); cleanup(true /*success*/); - }); + }, + QuickStepContract.getWindowCornerRadius(mContainer.asContext())); }); } @@ -804,53 +738,6 @@ 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 @@ -873,6 +760,7 @@ public class SplitSelectStateController { InteractionJankMonitorWrapper.end(mLaunchCuj); } mLaunchCuj = -1; + mLaunchingViewCuj = null; if (mSessionInstanceIds != null) { mStatsLogManager.logger() @@ -957,7 +845,7 @@ 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, @@ -968,21 +856,14 @@ public class SplitSelectStateController { 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 +886,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); }); } @@ -1075,4 +958,35 @@ public class SplitSelectStateController { } } } + + /** + * 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/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java index 5e42b9001b..4c6e4ff359 100644 --- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -27,9 +27,9 @@ 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 androidx.annotation.BinderThread; @@ -84,19 +84,23 @@ public class SplitWithKeyboardShortcutController { 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); + }); }); } @@ -110,17 +114,17 @@ public class SplitWithKeyboardShortcutController { 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, + 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 +140,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/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java index e44f14819a..828322b6bf 100644 --- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java +++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java @@ -39,7 +39,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 @@ -139,6 +139,10 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { final float aspectRatio = destinationBounds.width() / (float) 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,7 +153,7 @@ 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; @@ -164,22 +168,7 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { } 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); - } - + mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio)); // 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. @@ -225,6 +214,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 +446,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 +475,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/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java index 304b8f47eb..f3b984b81e 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; @@ -30,7 +31,8 @@ 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.wm.shell.shared.desktopmode.DesktopModeStatus; import java.util.List; import java.util.Set; @@ -40,8 +42,17 @@ import java.util.Set; */ public class SystemWindowManagerProxy extends WindowManagerProxy { + private final TISBindHelper mTISBindHelper; + public SystemWindowManagerProxy(Context context) { super(true); + mTISBindHelper = new TISBindHelper(context, binder -> {}); + } + + @Override + public void close() { + super.close(); + mTISBindHelper.onDestroy(); } @Override @@ -53,10 +64,28 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { @Override public boolean isInDesktopMode() { DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); + mTISBindHelper.getDesktopVisibilityController(); return desktopController != null && desktopController.areDesktopTasksVisible(); } + @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 isHomeVisible(Context context) { + return SystemUiProxy.INSTANCE.get(context).getHomeVisibilityState().isHomeVisible(); + } + @Override public int getRotation(Context displayInfoContext) { return displayInfoContext.getResources().getConfiguration().windowConfiguration diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java index 9a010429d3..b57360410e 100644 --- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java +++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java @@ -25,6 +25,7 @@ import android.util.Log; import androidx.annotation.Nullable; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.TaskbarManager; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.TouchInteractionService; @@ -108,6 +109,11 @@ public class TISBindHelper implements ServiceConnection { return mBinder == null ? null : mBinder.getTaskbarManager(); } + @Nullable + public DesktopVisibilityController getDesktopVisibilityController() { + return mBinder == null ? null : mBinder.getDesktopVisibilityController(); + } + /** * Sets flag whether a predictive back-to-home animation is in progress */ diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java index 98d363ef5c..498078b423 100644 --- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java +++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java @@ -22,13 +22,13 @@ import androidx.annotation.IntDef; import com.android.launcher3.util.IntArray; import java.lang.annotation.Retention; +import java.util.List; /** * 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; @@ -43,25 +43,25 @@ public class TaskGridNavHelper { private final IntArray mOriginalTopRowIds; private IntArray mTopRowIds; private IntArray mBottomRowIds; - private final int mFocusedTaskId; - public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) { - mFocusedTaskId = focusedTaskId; + public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, + List largeTileIds) { mOriginalTopRowIds = topIds.clone(); - generateTaskViewIdGrid(topIds, bottomIds); + generateTaskViewIdGrid(topIds, bottomIds, largeTileIds); } - 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); + private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray, + List largeTileIds) { - // Add the focused task to the beginning of both arrays if it exists. - if (hasFocusedTask) { - topRowIdArray.add(0, mFocusedTaskId); - bottomRowIdArray.add(0, mFocusedTaskId); + int maxSize = Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + + largeTileIds.size(); + int minSize = Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + + largeTileIds.size(); + + // Add Large tile task views first at the beginning + for (int i = 0; i < largeTileIds.size(); i++) { + topRowIdArray.add(i, largeTileIds.get(i)); + bottomRowIdArray.add(i, largeTileIds.get(i)); } // Fill in the shorter array with the ids from the longer one. diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 49f4e5f701..c7777d86a5 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; @@ -172,7 +170,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 +183,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); } /** @@ -247,8 +253,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(); } @@ -489,10 +493,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 @@ -528,21 +534,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 + ? Integer.MIN_VALUE + app.prefixOrderIndex + // 1000 is an arbitrary number to give room for multiple layers. + : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex); } } diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java index 66bff730bf..519ef60eca 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/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt index 09563f5527..915c9e5305 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/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 936f6a1c0a..6db0923563 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -15,19 +15,24 @@ */ package com.android.quickstep.views +import android.annotation.SuppressLint import android.content.Context import android.graphics.Point 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.util.AttributeSet import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R +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 @@ -40,7 +45,7 @@ import com.android.systemui.shared.recents.model.Task /** 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) { private val snapshotDrawParams = object : FullscreenDrawParams(context) { @@ -48,11 +53,11 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu override fun computeTaskCornerRadius(context: Context) = computeWindowCornerRadius(context) } - private val taskThumbnailViewPool = + private val taskThumbnailViewDeprecatedPool = ViewPool( context, this, - R.layout.task_thumbnail, + R.layout.task_thumbnail_deprecated, VIEW_POOL_MAX_SIZE, VIEW_POOL_INITIAL_SIZE ) @@ -82,80 +87,19 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu } 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))) + setIcon( + this, + ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_desktop_with_bg, + context.theme + ) + ) + setText(resources.getText(R.string.recent_task_desktop)) } 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) - - if (taskContainers.isEmpty()) { - return - } - - val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx - containerHeight -= 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. - 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) - ) - - // 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]" - ) - } - } - } - - override fun onRecycle() { - super.onRecycle() - visibility = VISIBLE - } - /** Updates this desktop task to the gives task list defined in `tasks` */ fun bind( tasks: List, @@ -169,60 +113,128 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu Log.d(TAG, sb.toString()) } cancelPendingLoadTasks() + taskContainers = + tasks.map { task -> + val snapshotView = + if (enableRefactorTaskThumbnail()) { + LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false) + } else { + taskThumbnailViewDeprecatedPool.view + } - 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 - ) + snapshotView, + // Add snapshotView to the front after initial views e.g. icon and + // background. + childCountAtInflation ) - } 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) - } - } - + taskContainers.forEach { it.bind() } setOrientationState(orientedState) } + override fun onRecycle() { + super.onRecycle() + visibility = VISIBLE + taskContainers.forEach { + if (!enableRefactorTaskThumbnail()) { + removeView(it.thumbnailViewDeprecated) + taskThumbnailViewDeprecatedPool.recycle(it.thumbnailViewDeprecated) + } + } + } + + @SuppressLint("RtlHardcoded") + override fun updateTaskSize( + lastComputedTaskSize: Rect, + lastComputedGridTaskSize: Rect, + lastComputedCarouselTaskSize: Rect + ) { + super.updateTaskSize( + lastComputedTaskSize, + lastComputedGridTaskSize, + lastComputedCarouselTaskSize + ) + if (taskContainers.isEmpty()) { + return + } + + val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx + + val containerWidth = layoutParams.width + val containerHeight = 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. + 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 positionInParent = it.task.positionInParent ?: ORIGIN + + // Position the task to the same position as it would be on the desktop + it.snapshotView.updateLayoutParams { + gravity = Gravity.LEFT or Gravity.TOP + width = (taskSize.width() * scaleWidth).toInt() + height = (taskSize.height() * scaleHeight).toInt() + leftMargin = (positionInParent.x * scaleWidth).toInt() + topMargin = + (positionInParent.y * scaleHeight).toInt() + + container.deviceProfile.overviewTaskThumbnailTopMarginPx + } + if (DEBUG) { + with(it.snapshotView.layoutParams as LayoutParams) { + Log.d( + TAG, + "onMeasure: task=${it.task.key} size=[$width,$height]" + + " margin=[$leftMargin,$topMargin]" + ) + } + } + } + } + override fun needsUpdate(dataChange: Int, flag: Int) = - if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false + if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag) + + override fun onIconLoaded(taskContainer: TaskContainer) { + // Update contentDescription of snapshotView only, individual task icon is unused. + taskContainer.snapshotView.contentDescription = taskContainer.task.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() {} @@ -235,23 +247,35 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu } } - 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) + + override fun launchWithoutAnimation( + isQuickSwitch: Boolean, + callback: (launched: Boolean) -> Unit + ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false) // Desktop tile can't be in split screen override fun confirmSecondSplitSelectApp(): Boolean = false @@ -260,8 +284,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu 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() { @@ -275,6 +298,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu private const val TAG = "DesktopTaskView" private const val DEBUG = false private const val VIEW_POOL_MAX_SIZE = 10 + // 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) 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 4a5b9e4cc1..0000000000 --- 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 0000000000..7b97c23bbc --- /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.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.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY) + SplitBannerConfig.SPLIT_GRID_BANNER_SMALL + else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + else -> + if (splitBounds.leftTaskPercent > 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 0000000000..c8930165b5 --- /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 e024995aa3..6bbd6b2c15 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 4ea77532bc..f17be05d42 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 4dde635da7..bdca596a92 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java @@ -123,8 +123,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 +212,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 +259,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) { diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index d6a3376c53..92c1e9396f 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -33,12 +33,10 @@ 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.shared.split.SplitScreenConstants.PersistentSnapPosition /** * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks @@ -51,7 +49,7 @@ 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) { // TODO(b/336612373): Support new TTV for GroupedTaskView var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null private set @@ -67,47 +65,18 @@ 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( + taskContainers[0].snapshotView, + taskContainers[1].snapshotView, + widthSize, + heightSize, + splitBoundsConfig, + container.deviceProfile, + layoutDirection == LAYOUT_DIRECTION_RTL, + inSplitSelection, + ) + if (!enableOverviewIconMenu()) { updateIconPlacement() } @@ -133,6 +102,7 @@ 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 ), @@ -141,28 +111,15 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu 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 ) ) - 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) } + taskContainers.forEach { it.bind() } + + this.splitBoundsConfig = splitBoundsConfig + taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig } setOrientationState(orientedState) } @@ -203,6 +160,8 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val splitBoundsConfig = splitBoundsConfig ?: return val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID + if (enableOverviewIconMenu()) { val groupedTaskViewSizes = pagedOrientationHandler.getGroupedTaskViewSizes( @@ -221,20 +180,22 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu layoutParams.width, isRtl, container.deviceProfile, - splitBoundsConfig + splitBoundsConfig, + inSplitSelection ) } else { pagedOrientationHandler.setSplitIconParams( taskContainers[0].iconView.asView(), taskContainers[1].iconView.asView(), taskIconHeight, - taskContainers[0].thumbnailViewDeprecated.measuredWidth, - taskContainers[0].thumbnailViewDeprecated.measuredHeight, + taskContainers[0].snapshotView.measuredWidth, + taskContainers[0].snapshotView.measuredHeight, measuredHeight, measuredWidth, isRtl, container.deviceProfile, - splitBoundsConfig + splitBoundsConfig, + inSplitSelection ) } } @@ -242,17 +203,13 @@ 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 @@ -271,8 +228,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) } /** @@ -296,7 +256,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu isQuickSwitch, snapPosition ) - Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}") + Log.d( + TAG, + "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView" + ) } } @@ -324,7 +287,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu // Check which of the two apps was selected if ( taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) || - taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition) + taskContainers[1].snapshotView.containsPoint(lastTouchDownPosition) ) { return 1 } 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 bb4a7ecda6..0000000000 --- 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 0000000000..583207f4f1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconView.kt @@ -0,0 +1,159 @@ +/* + * 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.Utilities +import com.android.launcher3.util.MultiValueAlpha +import com.android.launcher3.views.ActivityContext +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. + */ +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 + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + init { + multiValueAlpha.setUpdateVisibility(true) + } + + /** Sets a [Drawable] to be displayed. */ + override fun setDrawable(d: Drawable?) { + drawable?.callback = null + + drawable = d + 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) + } + + /** + * 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 + + companion object { + private const val NUM_ALPHA_CHANNELS = 2 + private const val INDEX_CONTENT_ALPHA = 0 + private const val INDEX_MODAL_ALPHA = 1 + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index e48a7c6053..73edb9e8fa 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -16,6 +16,7 @@ package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON; @@ -26,7 +27,6 @@ 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; @@ -54,6 +54,7 @@ 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; @@ -91,10 +92,12 @@ public class LauncherRecentsView extends RecentsView 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/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index d7c7857653..287a34d768 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -36,10 +36,13 @@ 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.enableDesktopTaskAlphaAnimation; import static com.android.launcher3.Flags.enableGridOnlyOverview; +import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile; import static com.android.launcher3.Flags.enableRefactorTaskThumbnail; 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; @@ -57,6 +60,7 @@ 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.LogUtils.splitFailureMessage; import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN; @@ -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; @@ -192,8 +197,14 @@ import com.android.quickstep.TaskViewUtils; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.ViewUtils; 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.recents.viewmodel.RecentsViewModel; import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.AnimUtils; @@ -202,6 +213,7 @@ 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.RecentsViewUtils; import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; @@ -212,7 +224,6 @@ 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.ThumbnailData; @@ -221,29 +232,33 @@ 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.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import kotlin.Unit; 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> extends PagedView implements Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, TaskVisualsChangeListener { @@ -306,8 +321,29 @@ public abstract class RecentsView RUNNING_TASK_ATTACH_ALPHA = + new FloatProperty("runningTaskAttachAlpha") { + @Override + public void setValue(RecentsView recentsView, float v) { + TaskView runningTask = recentsView.getRunningTaskView(); + if (runningTask == null) { + return; + } + runningTask.setAttachAlpha(v); + } + + @Override + public Float get(RecentsView recentsView) { + TaskView runningTask = recentsView.getRunningTaskView(); + if (runningTask == null) { + return null; + } + return runningTask.getAttachAlpha(); + } + }; + 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,7 +352,6 @@ public abstract class RecentsView COLOR_TINT = new FloatProperty("colorTint") { @@ -390,7 +425,7 @@ public abstract class RecentsView mSizeStrategy; @Nullable @@ -549,6 +581,7 @@ public abstract class RecentsView( () -> PackageManagerWrapper.getInstance() @@ -646,9 +681,10 @@ public abstract class RecentsView new RecentsRotationStateRepositoryImpl(mOrientationState)); + + recentsDependencies.provide(RecentsDeviceProfileRepository.class, + () -> 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); @@ -1017,15 +1081,20 @@ public abstract class RecentsView container.getIconView().getDrawable() != null)) { - tv.onTaskListVisibilityChanged(true /* visible */); + taskView.onTaskListVisibilityChanged(true /* visible */); } } } @@ -1049,42 +1117,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 @@ -1113,6 +1175,7 @@ public abstract class RecentsView taskGroups) { if (mPendingAnimation != null) { - mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); + final List finalTaskGroups = taskGroups; + mPendingAnimation.addEndListener(success -> applyLoadPlan(finalTaskGroups)); return; } @@ -1780,12 +1848,15 @@ public abstract class RecentsView= 0; i--) { @@ -1819,7 +1895,7 @@ public abstract class RecentsView nonMinimizedTasks = + ((DesktopTask) groupTask).tasks.stream() + .filter(task -> !task.isMinimized) + .toList(); + ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState, + mTaskOverlayFactory); mDesktopTaskView = (DesktopTaskView) taskView; } else { Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2 @@ -1851,11 +1932,14 @@ public abstract class RecentsView 0) { - newFocusedTaskView = getTaskViewAt(0); + int newFocusedTaskViewIndex = mUtils.getFocusedTaskIndex(taskGroups); + if (newFocusedTaskView == null && getTaskViewCount() > newFocusedTaskViewIndex) { + newFocusedTaskView = getTaskViewAt(newFocusedTaskViewIndex); } - mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview() - ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID; + + setFocusedTaskViewId(newFocusedTaskView != null && !enableGridOnlyOverview() + ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID); + updateTaskSize(); updateChildTaskOrientations(); @@ -1895,8 +1979,8 @@ public abstract class RecentsView 0) { - targetPage = indexOfChild(requireTaskViewAt(0)); + } else if (getTaskViewCount() > newFocusedTaskViewIndex) { + targetPage = indexOfChild(requireTaskViewAt(newFocusedTaskViewIndex)); } } if (targetPage != -1 && mCurrentPage != targetPage) { @@ -1921,6 +2005,7 @@ public abstract class RecentsView= 0; i--) { - if (i == stubRunningTaskIndex) { + for (TaskView taskView : getTaskViews()) { + if (isGestureActive() && taskView.isRunningTask()) { + // 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. continue; } - removeView(requireTaskViewAt(i)); + removeView(taskView); } if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) { removeView(mClearAllButton); @@ -1961,14 +2044,13 @@ 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(); @@ -2001,6 +2082,7 @@ public abstract class RecentsView tasksToUpdate = containers.stream() .map(TaskContainer::getTask) .collect(Collectors.toCollection(ArrayList::new)); - if (enableRefactorTaskThumbnail()) { - visibleTaskIds.addAll( - tasksToUpdate.stream().map((task) -> task.key.id).toList()); - } if (mTmpRunningTasks != null) { for (Task t : mTmpRunningTasks) { // Skip loading if this is the task that we are animating into @@ -2411,6 +2474,10 @@ public abstract class RecentsView task == t); } } + if (enableRefactorTaskThumbnail()) { + visibleTaskIds.addAll( + tasksToUpdate.stream().map((task) -> task.key.id).toList()); + } if (tasksToUpdate.isEmpty()) { continue; } @@ -2424,7 +2491,7 @@ public abstract class RecentsView { - unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); - setCurrentPage(0); - LayoutUtils.setViewEnabled(mActionsView, true); - if (mOrientationState.setGestureActive(false)) { - updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); - } - }); + post(this::onReset); + } + + private void onReset() { + if (enableRefactorTaskThumbnail()) { + mRecentsViewModel.onReset(); + } + unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); + setCurrentPage(0); + LayoutUtils.setViewEnabled(mActionsView, true); + if (mOrientationState.setGestureActive(false)) { + updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); + } } public int getRunningTaskViewId() { @@ -2569,8 +2653,7 @@ public abstract class RecentsView task.key.id).toArray(); TaskView matchingTaskView = null; if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) { - // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single + // 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) { @@ -2823,6 +2923,7 @@ public abstract class RecentsView 1; boolean needDesktopTask = hasDesktopTask(runningTasks); @@ -2831,20 +2932,20 @@ public abstract class RecentsView setCurrentPage(getRunningTaskIndex())); setRunningTaskViewShowScreenshot(false); setRunningTaskHidden(runningTaskTileHidden); @@ -2909,21 +3014,20 @@ 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); + for (TaskView taskView : getTaskViews()) { + taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); } } } @@ -2970,9 +3096,7 @@ public abstract class RecentsView largeTasksIndices = new HashSet<>(); int focusedTaskShift = 0; - int focusedTaskWidthAndSpacing = 0; + int largeTaskWidthAndSpacing = 0; int snappedTaskRowWidth = 0; int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage(); TaskView snappedTaskView = getTaskViewAt(snappedPage); TaskView homeTaskView = getHomeTaskView(); 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++) { @@ -3049,12 +3163,11 @@ public abstract class RecentsView focusedTaskIndex) { // For tasks after the focused task, shift by focused task's width and spacing. gridTranslations[i] += - mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; + mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing; } else { // For task before the focused task, accumulate the width and spacing to // calculate the distance focused task need to shift. @@ -3080,7 +3199,7 @@ public abstract class RecentsView startRebalanceAfter) { mTopRowIdSet.remove(taskViewId); isTopRow = topRowWidth <= bottomRowWidth; @@ -3106,7 +3225,7 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex) { + if (largeTasksIndices.contains(j)) { continue; } widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; @@ -3125,7 +3244,7 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex) { + if (largeTasksIndices.contains(j)) { continue; } widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; @@ -3175,12 +3294,22 @@ public abstract class RecentsView resetFromSplitSelectionState()))); + firstFloatingTaskView.setContentDescription(splitAnimInitProps.getContentDescription()); // SplitInstructionsView: animate in safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView()); @@ -3449,11 +3577,13 @@ 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()); @@ -3627,8 +3756,7 @@ public abstract class RecentsView= screenEnd - mPageSpacing; } if (shouldRebalance) { - updateGridProperties(/*isTaskDismissal=*/ true, - highestVisibleTaskIndex); + updateGridProperties(highestVisibleTaskIndex); updateScrollSynchronously(); } } @@ -4087,9 +4216,8 @@ 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); int alphaInt = Math.round(alpha * 255); @@ -4447,6 +4570,13 @@ public abstract class RecentsView getTaskViews() { + return mUtils.getTaskViews(getTaskViewCount(), this::requireTaskViewAt); + } + public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) { mOnEmptyMessageUpdatedListener = listener; } @@ -4543,9 +4673,10 @@ public abstract class RecentsView 0; - if (isModalGridWithoutFocusedTask) { + boolean shouldCalculateOffsetForAllTasks = showAsGrid + && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile()) + && mTaskModalness > 0; + if (shouldCalculateOffsetForAllTasks) { modalMidpoint = indexOfChild(mSelectedTask); } @@ -4584,7 +4715,7 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() @@ -4756,23 +4891,21 @@ public abstract class RecentsView{ - thumbnail.refreshSplashView(); + builder.addOnFrameCallback(() -> { + if (!enableRefactorTaskThumbnail()) { + taskContainer.getThumbnailViewDeprecated().refreshSplashView(); + } mSplitHiddenTaskView.updateSnapshotRadius(); }); } else if (isInitiatingSplitFromTaskView) { + if (Flags.enableHoverOfChildElementsInTaskview()) { + mSplitHiddenTaskView.setBorderEnabled(false); + } // Splitting from Overview for fullscreen task createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, true /* dismissingForSplitSelection*/); @@ -4885,17 +5023,21 @@ public abstract class RecentsView { + int targetSysUiFlags = taskView.getTaskContainers().getFirst().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 +5435,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,19 +5445,7 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() .addOverviewToAppAnim(mPendingAnimation, interpolator)); mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); + if (taskView instanceof DesktopTaskView && mRemoteTargetHandles != null) { + mPendingAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(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 +5475,13 @@ public abstract class RecentsView fullyVisibleTaskIds = new HashSet<>(); + + // Update the task data for the in/visible children + for (int i = 0; i < getTaskViewCount(); i++) { + TaskView taskView = requireTaskViewAt(i); + List containers = taskView.getTaskContainers(); + if (containers.isEmpty()) { + continue; + } + boolean isFullyVisible; + if (showAsGrid()) { + isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart, + screenEnd); + } else { + isFullyVisible = i == centerPageIndex; + } + if (isFullyVisible) { + List taskIds = containers.stream().map( + taskContainer -> taskContainer.getTask().key.id).toList(); + fullyVisibleTaskIds.addAll(taskIds); + } + } + mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds); + } } @Override @@ -5425,7 +5622,7 @@ 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); } @@ -5841,7 +6055,7 @@ public abstract class RecentsView getEventDispatcher(float navbarRotation) { @@ -5875,9 +6089,7 @@ public abstract class RecentsView updatedThumbnails = mUtils.screenshotTasks(taskView, + mRecentsAnimationController); + if (enableRefactorTaskThumbnail()) { + mHelper.switchToScreenshot(taskView, updatedThumbnails, onFinishRunnable); + } else { + setRunningTaskViewShowScreenshot(true, updatedThumbnails); + ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } - ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } /** @@ -5980,9 +6184,12 @@ public abstract class RecentsView?, + onFinishRunnable: Runnable, + ) { + // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside + // viewAttachedScope. + recentsViewModel.setRunningTaskShowScreenshot(true) + viewAttachedScope.launch { + recentsViewModel.waitForRunningTaskShowScreenshotToUpdate() + recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails) + ViewUtils.postFrameDrawn(taskView, onFinishRunnable) + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java index 3d994e89a8..f6393e4717 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; @@ -42,9 +40,8 @@ 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; /** @@ -57,7 +54,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; @@ -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 0000000000..57d68a092e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt @@ -0,0 +1,192 @@ +/* + * 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.Intent +import android.graphics.Bitmap +import android.view.View +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag +import com.android.launcher3.LauncherSettings +import com.android.launcher3.model.data.ItemInfoWithIcon +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.pm.UserCache +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskUtils +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.recents.di.getScope +import com.android.quickstep.recents.di.inject +import com.android.quickstep.recents.viewmodel.TaskContainerViewModel +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.task.viewmodel.TaskContainerData +import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel +import com.android.systemui.shared.recents.model.Task + +/** 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) + lateinit var taskContainerData: TaskContainerData + + private val taskThumbnailViewModel: TaskThumbnailViewModel by + RecentsDependencies.inject(snapshotView) + + // TODO(b/335649589): Ideally create and obtain this from DI. + private val taskContainerViewModel: TaskContainerViewModel by lazy { + TaskContainerViewModel( + sysUiStatusNavFlagsUseCase = RecentsDependencies.get(), + getThumbnailUseCase = RecentsDependencies.get(), + splashAlphaUseCase = RecentsDependencies.get(), + ) + } + + init { + if (enableRefactorTaskThumbnail()) { + require(snapshotView is TaskThumbnailView) + taskContainerData = RecentsDependencies.get(this) + RecentsDependencies.getScope(snapshotView).apply { + val taskViewScope = RecentsDependencies.getScope(taskView) + linkTo(taskViewScope) + + val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer) + linkTo(taskContainerScope) + } + } else { + require(snapshotView is TaskThumbnailViewDeprecated) + } + } + + val splitAnimationThumbnail: Bitmap? + get() = + if (enableRefactorTaskThumbnail()) { + taskContainerViewModel.getThumbnail(task.key.id) + } else { + thumbnailViewDeprecated.thumbnail + } + + val thumbnailView: TaskThumbnailView + get() { + require(enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailView + } + + val thumbnailViewDeprecated: TaskThumbnailViewDeprecated + get() { + require(!enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailViewDeprecated + } + + // TODO(b/334826842): Support shouldShowSplashView for new TTV. + val shouldShowSplashView: Boolean + get() = + if (enableRefactorTaskThumbnail()) + taskContainerViewModel.shouldShowThumbnailSplash(task.key.id) + else thumbnailViewDeprecated.shouldShowSplashView() + + val sysUiStatusNavFlags: Int + get() = + if (enableRefactorTaskThumbnail()) + taskContainerViewModel.getSysUiStatusNavFlags(task.key.id) + else thumbnailViewDeprecated.sysUiStatusNavFlags + + /** 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 + taskView.recentsView?.let { screenId = it.indexOfChild(taskView) } + if (privateSpaceRestrictAccessibilityDrag()) { + if ( + UserCache.getInstance(taskView.context) + .getUserInfo(componentKey.user) + .isPrivate + ) { + runtimeStatusFlags = + runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE + } + } + } + + fun bind() { + digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition) + if (enableRefactorTaskThumbnail()) { + bindThumbnailView() + } else { + thumbnailViewDeprecated.bind(task, overlay) + } + overlay.init() + } + + fun destroy() { + digitalWellBeingToast?.destroy() + if (enableRefactorTaskThumbnail()) { + taskView.removeView(thumbnailView) + } + snapshotView.scaleX = 1f + snapshotView.scaleY = 1f + overlay.destroy() + } + + fun bindThumbnailView() { + taskThumbnailViewModel.bind(task.key.id) + } + + fun setOverlayEnabled(enabled: Boolean) { + if (!enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.setOverlayEnabled(enabled) + } + } + + fun addChildForAccessibility(outChildren: ArrayList) { + addAccessibleChildToList(iconView.asView(), outChildren) + addAccessibleChildToList(snapshotView, outChildren) + showWindowsView?.let { addAccessibleChildToList(it, outChildren) } + digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) } + } + + private fun addAccessibleChildToList(view: View, outChildren: ArrayList) { + if (view.includeForAccessibility()) { + outChildren.add(view) + } else { + view.addChildrenForAccessibility(outChildren) + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java index eda58c5907..63bc509d7b 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -18,6 +18,7 @@ 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.Flags.enableRefactorTaskThumbnail; 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; @@ -54,7 +55,6 @@ 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. @@ -237,12 +237,12 @@ public class TaskMenuView extends AbstractFloatingView { mContainer.getDragLayer().getDescendantRectRelativeToSelf( enableOverviewIconMenu() ? getIconView().findViewById(R.id.icon_view_menu_anchor) - : taskContainer.getThumbnailViewDeprecated(), + : taskContainer.getSnapshotView(), sTempRect); Rect insets = mContainer.getDragLayer().getInsets(); BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); params.width = orientationHandler.getTaskMenuWidth( - taskContainer.getThumbnailViewDeprecated(), deviceProfile, + taskContainer.getSnapshotView(), deviceProfile, taskContainer.getStagePosition()); // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start params.gravity = Gravity.LEFT; @@ -276,10 +276,10 @@ public class TaskMenuView extends AbstractFloatingView { // 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, + mTaskContainer.getSnapshotView(), deviceProfile, taskInsetMargin, getIconView())); setTranslationY(orientationHandler.getTaskMenuY( - thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(), + thumbnailAlignedY, mTaskContainer.getSnapshotView(), mTaskContainer.getStagePosition(), this, taskInsetMargin, getIconView())); } @@ -315,7 +315,7 @@ public class TaskMenuView extends AbstractFloatingView { .createRevealAnimator(this, closing, revealAnimationStartProgress); mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED : Interpolators.DECELERATE); - + AnimatorSet.Builder openCloseAnimatorBuilder = mOpenCloseAnimator.play(mRevealAnimator); if (enableOverviewIconMenu()) { IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView(); @@ -333,11 +333,13 @@ public class TaskMenuView extends AbstractFloatingView { closing ? mMenuTranslationYBeforeOpen : mMenuTranslationYBeforeOpen + additionalTranslationY); translationYAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(translationYAnim); ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat( iconAppChip.getMenuTranslationY(), MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY); menuTranslationYAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(menuTranslationYAnim); float additionalTranslationX = 0; if (mContainer.getDeviceProfile().isLandscape @@ -353,20 +355,27 @@ public class TaskMenuView extends AbstractFloatingView { closing ? mMenuTranslationXBeforeOpen : mMenuTranslationXBeforeOpen - additionalTranslationX); translationXAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(translationXAnim); ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat( iconAppChip.getMenuTranslationX(), MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX); menuTranslationXAnim.setInterpolator(EMPHASIZED); - - mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim, - menuTranslationXAnim, menuTranslationYAnim); + openCloseAnimatorBuilder.with(menuTranslationXAnim); + } + openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); + if (enableRefactorTaskThumbnail()) { + mRevealAnimator.addUpdateListener(animation -> { + float animatedFraction = animation.getAnimatedFraction(); + float openProgress = closing ? (1 - animatedFraction) : animatedFraction; + mTaskContainer.getTaskContainerData() + .getTaskMenuOpenProgress().setValue(openProgress); + }); + } else { + openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat( + mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA, + closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); } - 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) { diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt index 659cc0c254..19d706fbb7 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 4283d0e4cf..56ca043a79 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,7 +42,6 @@ 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; @@ -99,36 +95,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); @@ -160,8 +126,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); @@ -200,17 +164,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 +251,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) { @@ -415,31 +334,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(); } @@ -544,7 +438,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(); @@ -617,6 +513,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/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 94ebc5a2d0..2ed6ae66c8 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 @@ -32,6 +31,7 @@ import android.util.AttributeSet import android.util.FloatProperty import android.util.Log import android.view.Display +import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.View.OnClickListener @@ -46,21 +46,16 @@ import androidx.annotation.VisibleForTesting import androidx.core.view.updateLayoutParams import com.android.app.animation.Interpolators import com.android.launcher3.Flags.enableCursorHoverStates -import com.android.launcher3.Flags.enableFocusOutline 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.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.testing.TestLogging import com.android.launcher3.testing.shared.TestProtocol import com.android.launcher3.util.CancellableTask @@ -68,6 +63,7 @@ import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Executors 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 @@ -81,15 +77,12 @@ import com.android.launcher3.util.rects.set import com.android.launcher3.views.ActivityContext import com.android.quickstep.RecentsModel import com.android.quickstep.RemoteAnimationTargets -import com.android.quickstep.TaskAnimationManager 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.task.viewmodel.TaskViewModel import com.android.quickstep.util.ActiveGestureErrorDetector import com.android.quickstep.util.ActiveGestureLog import com.android.quickstep.util.BorderAnimator @@ -112,7 +105,8 @@ constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0, focusBorderAnimator: BorderAnimator? = null, - hoverBorderAnimator: BorderAnimator? = null + hoverBorderAnimator: BorderAnimator? = null, + private val type: TaskViewType = TaskViewType.SINGLE, ) : FrameLayout(context, attrs), ViewPool.Reusable { /** * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which @@ -122,34 +116,30 @@ 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 - } - } + private lateinit var taskViewModel: TaskViewModel - 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 + val isLargeTile: Boolean + get() = + this == recentsView?.focusedTaskView || + (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP) val taskCornerRadius: Float get() = currentFullscreenParams.cornerRadius @@ -166,9 +156,9 @@ constructor( get() = taskContainers[0].task @get:Deprecated("Use [taskContainers] instead.") - val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated - /** Returns the first thumbnailView of the TaskView. */ - get() = taskContainers[0].thumbnailViewDeprecated + val firstSnapshotView: View + /** Returns the first snapshotView of the TaskView. */ + get() = taskContainers[0].snapshotView @get:Deprecated("Use [taskContainers] instead.") val firstItemInfo: ItemInfo @@ -208,14 +198,14 @@ 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 @@ -230,21 +220,21 @@ 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) @@ -398,14 +388,23 @@ constructor( applyTranslationX() } - protected var stableAlpha = 1f + private val taskViewAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS) + + protected var stableAlpha set(value) { - field = value - alpha = stableAlpha + taskViewAlpha.get(ALPHA_INDEX_STABLE).value = value } + get() = taskViewAlpha.get(ALPHA_INDEX_STABLE).value + + var attachAlpha + set(value) { + taskViewAlpha.get(ALPHA_INDEX_ATTACH).value = value + } + get() = taskViewAlpha.get(ALPHA_INDEX_ATTACH).value protected var shouldShowScreenshot = false get() = !isRunningTask || field + private set /** Enable or disable showing border on hover and focus change */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @@ -421,6 +420,26 @@ constructor( focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) } + /** + * 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 + Log.d( + TAG, + "${taskIds.contentToString()} - setting border animator visibility to: $field", + ) + hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true) + } + + // Used to cache thumbnail bounds to avoid recalculating on every hover move. + private var thumbnailBounds = Rect() + private var focusTransitionProgress = 1f set(value) { field = value @@ -433,7 +452,7 @@ constructor( FOCUS_TRANSITION, FOCUS_TRANSITION_INDEX_COUNT, { x: Float, y: Float -> x * y }, - 1f + 1f, ) private val focusTransitionFullscreen = focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN) @@ -459,27 +478,28 @@ constructor( init { setOnClickListener { _ -> onClick() } - val keyboardFocusHighlightEnabled = - (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline()) + + if (enableRefactorTaskThumbnail()) { + taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type) + } + val cursorHoverStatesEnabled = enableCursorHoverStates() - setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled) + setWillNotDraw(!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 + ?: 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, + ), + ) this.hoverBorderAnimator = hoverBorderAnimator ?: if (cursorHoverStatesEnabled) @@ -492,8 +512,8 @@ constructor( this, it.getColor( R.styleable.TaskView_hoverBorderColor, - BorderAnimator.DEFAULT_BORDER_COLOR - ) + BorderAnimator.DEFAULT_BORDER_COLOR, + ), ) else null } @@ -503,7 +523,7 @@ constructor( public override fun onFocusChanged( gainFocus: Boolean, direction: Int, - previouslyFocusedRect: Rect? + previouslyFocusedRect: Rect?, ) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) if (borderEnabled) { @@ -514,20 +534,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 @@ -570,20 +598,23 @@ constructor( it.right = width it.bottom = height } + if (enableHoverOfChildElementsInTaskview()) { + getThumbnailBounds(thumbnailBounds) + } } override fun onRecycle() { resetPersistentViewTransforms() + attachAlpha = 1f // 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 taskContainers.forEach { it.destroy() } } @@ -597,7 +628,7 @@ constructor( addAction( AccessibilityAction( R.id.action_close, - context.getText(R.string.accessibility_close) + context.getText(R.string.accessibility_close), ) ) @@ -621,7 +652,7 @@ constructor( 1, it.taskViewCount - it.indexOfChild(this@TaskView) - 1, 1, - false + false, ) } } @@ -654,8 +685,9 @@ constructor( open fun bind( task: Task, orientedState: RecentsOrientedState, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ) { + cancelPendingLoadTasks() taskContainers = listOf( @@ -664,10 +696,12 @@ constructor( R.id.snapshot, R.id.icon, R.id.show_windows, + R.id.digital_wellbeing_toast, STAGE_POSITION_UNDEFINED, - taskOverlayFactory + taskOverlayFactory, ) ) + taskContainers.forEach { it.bind() } setOrientationState(orientedState) } @@ -676,42 +710,34 @@ constructor( @IdRes thumbnailViewId: Int, @IdRes iconViewId: Int, @IdRes showWindowViewId: Int, + @IdRes digitalWellbeingBannerId: Int, @StagePosition stagePosition: Int, - taskOverlayFactory: TaskOverlayFactory + 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( - task, - thumbnailView, - thumbnailViewDeprecated, - iconView, - TransformingTouchDelegate(iconView.asView()), - stagePosition, - DigitalWellBeingToast(container, this), - findViewById(showWindowViewId)!!, - taskOverlayFactory - ) - .apply { - if (enableRefactorTaskThumbnail()) { - thumbnailViewDeprecated.setTaskOverlay(overlay) - bindThumbnailView() - } else { - thumbnailViewDeprecated.bind(task, overlay) + val snapshotView = + if (enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.visibility = GONE + val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated) + LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false).also { + addView(it, indexOfSnapshotView, thumbnailViewDeprecated.layoutParams) } + } else { + thumbnailViewDeprecated } + val iconView = getOrInflateIconView(iconViewId) + val digitalWellBeingToast = findViewById(digitalWellbeingBannerId)!! + return TaskContainer( + this, + task, + snapshotView, + iconView, + TransformingTouchDelegate(iconView.asView()), + stagePosition, + digitalWellBeingToast, + findViewById(showWindowViewId)!!, + taskOverlayFactory, + ) } protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon { @@ -726,8 +752,6 @@ constructor( .inflate() as TaskViewIcon } - protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized - fun containsMultipleTasks() = taskContainers.size > 1 /** @@ -748,7 +772,7 @@ constructor( protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) { taskContainers.forEach { it.overlay.updateOrientationState(orientationState) - it.digitalWellBeingToast?.initialize(it.task) + it.digitalWellBeingToast?.initialize() } } @@ -756,10 +780,10 @@ constructor( * Updates TaskView scaling and translation required to support variable width if enabled, while * ensuring TaskView fits into screen in fullscreen. */ - fun updateTaskSize( + open fun updateTaskSize( lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect, - lastComputedCarouselTaskSize: Rect + lastComputedCarouselTaskSize: Rect, ) { val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx val taskWidth = lastComputedTaskSize.width() @@ -771,9 +795,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 { @@ -799,10 +824,8 @@ constructor( } 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 @@ -819,6 +842,7 @@ constructor( taskContainers[0].snapshotView.updateLayoutParams { topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx } + taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() } } /** Returns the thumbnail's bounds, optionally relative to the screen. */ @@ -830,7 +854,7 @@ constructor( if (relativeToDragLayer) { container.dragLayer.getDescendantRectRelativeToSelf( it.snapshotView, - thumbnailBounds + thumbnailBounds, ) } else { thumbnailBounds.set(it.snapshotView) @@ -862,7 +886,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) } @@ -878,19 +903,15 @@ constructor( 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) } } } @@ -909,6 +930,21 @@ constructor( pendingIconLoadRequests.clear() } + protected open fun onIconLoaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, taskContainer.task.icon) + if (enableOverviewIconMenu()) { + setText(taskContainer.iconView, taskContainer.task.title) + } + taskContainer.digitalWellBeingToast?.initialize() + } + + protected open fun onIconUnloaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, null) + if (enableOverviewIconMenu()) { + setText(taskContainer.iconView, null) + } + } + protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) { with(iconView) { if (icon != null) { @@ -934,9 +970,14 @@ constructor( 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 +997,7 @@ constructor( return } val callbackList = - launchTasks()?.apply { + launchWithAnimation()?.apply { add { Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted") } @@ -968,16 +1009,110 @@ constructor( .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() + } else { + launchAsStaticTile() + } + } + + private fun launchAsLiveTile(): RunnableList? { + val recentsView = recentsView ?: return null + val remoteTargetHandles = recentsView.remoteTargetHandles + 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.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 = 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, + ) + 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? { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", - taskIds.contentToString() + taskIds.contentToString(), ) val opts = container.getActivityLaunchOptions(this, null).apply { @@ -989,45 +1124,45 @@ constructor( ) { 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, + ) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", - taskIds.contentToString() + taskIds.contentToString(), ) val firstContainer = taskContainers[0] val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) @@ -1036,7 +1171,7 @@ constructor( // 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() + 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 +1193,7 @@ constructor( 0, 0, Executors.MAIN_EXECUTOR.handler, - { callback(true) } + { callback(true) }, ) { failureListener.onTransitionFinished() } @@ -1067,11 +1202,9 @@ constructor( if (isQuickSwitch) { setFreezeRecentTasksReordering() } - // TODO(b/334826842) add splash functionality to new TTV - if (!enableRefactorTaskThumbnail()) { - disableStartingWindow = - firstContainer.thumbnailViewDeprecated.shouldShowSplashView() - } + // TODO(b/334826842) no work required - add splash functionality to new TTV - + // cold start e.g. restart device. Small splash moving to bigger splash + disableStartingWindow = firstContainer.shouldShowSplashView } Executors.UI_HELPER_EXECUTOR.execute { if ( @@ -1082,98 +1215,20 @@ constructor( // 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") } @@ -1185,7 +1240,7 @@ constructor( recentsView?.initiateSplitSelect( this, splitPositionOption.stagePosition, - SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition) + SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition), ) } @@ -1204,11 +1259,11 @@ constructor( this, container.task, container.iconView.drawable, - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, /* intent */ - null, /* user */ - null, - container.itemInfo + container.snapshotView, + container.splitAnimationThumbnail, + /* intent */ null, + /* user */ null, + container.itemInfo, ) } @@ -1239,10 +1294,17 @@ constructor( 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 (enableHoverOfChildElementsInTaskview()) { + recentsView.setTaskBorderEnabled(true) + } } } else if (container.deviceProfile.isTablet) { val alignedOptionIndex = @@ -1262,9 +1324,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 +1357,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 +1368,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 +1384,7 @@ constructor( it.showWindowsView?.let { showWindowsView -> updateFilterCallback( showWindowsView, - getFilterUpdateCallback(it.task.key.packageName) + getFilterUpdateCallback(it.task.key.packageName), ) } } @@ -1350,7 +1420,7 @@ constructor( private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) { taskContainers.forEach { it.iconView.setContentAlpha(focusTransitionProgress) - it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress) + it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - focusTransitionProgress } } @@ -1380,11 +1450,10 @@ constructor( open fun setColorTint(amount: Float, tintColor: Int) { taskContainers.forEach { if (!enableRefactorTaskThumbnail()) { - // TODO(b/334832108) Add scrim to new TTV it.thumbnailViewDeprecated.dimAlpha = amount } it.iconView.setIconColorTint(tintColor, amount) - it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount) + it.digitalWellBeingToast?.setColorTint(tintColor, amount) } } @@ -1398,7 +1467,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 +1475,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() } } } @@ -1432,14 +1498,13 @@ constructor( scaleX = scale scaleY = scale if (enableRefactorTaskThumbnail()) { - taskViewData.scale.value = scale + taskViewModel.updateScale(scale) } updateSnapshotRadius() } protected open fun applyThumbnailSplashAlpha() { if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV taskContainers.forEach { it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha) } @@ -1484,7 +1549,9 @@ constructor( protected open fun updateSnapshotRadius() { updateCurrentFullscreenParams() taskContainers.forEach { - it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams()) + if (!enableRefactorTaskThumbnail()) { + it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams()) + } it.overlay.setFullscreenParams(getThumbnailFullscreenParams()) } } @@ -1503,27 +1570,24 @@ constructor( private fun onModalnessUpdated(modalness: Float) { taskContainers.forEach { it.iconView.setModalAlpha(1 - modalness) - it.digitalWellBeingToast?.updateBannerOffset(modalness) + it.digitalWellBeingToast?.bannerOffsetPercentage = modalness } } - /** 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() { nonGridTranslationX = 0f 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 +1603,8 @@ constructor( } dismissScale = 1f translationZ = 0f - alpha = stableAlpha setIconScaleAndDim(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) = @@ -1604,65 +1663,13 @@ constructor( override fun close() {} } - /** 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) + private fun MotionEvent.isWithinThumbnailBounds(): Boolean { + return thumbnailBounds.contains(x.toInt(), y.toInt()) + } - 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) } } @@ -1678,6 +1685,11 @@ constructor( const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1 const val FOCUS_TRANSITION_INDEX_COUNT = 2 + private const val ALPHA_INDEX_STABLE = 0 + private const val ALPHA_INDEX_ATTACH = 1 + + private const val NUM_ALPHA_CHANNELS = 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 @@ -1688,7 +1700,7 @@ constructor( Interpolators.clampToProgress( Interpolators.FAST_OUT_SLOW_IN, 1f - FOCUS_TRANSITION_THRESHOLD, - 1f + 1f, )!! private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect()) 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 0000000000..b2a32a96c8 --- /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/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 0000000000..37a07c3647 --- /dev/null +++ b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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) + 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") + 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 0000000000..e4b8069b7f --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt @@ -0,0 +1,141 @@ +/* + * 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 } + 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 0000000000..47f393ffc1 --- /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/flyout/BubbleBarFlyoutViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt new file mode 100644 index 0000000000..537a755c21 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt @@ -0,0 +1,159 @@ +/* + * 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.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, onLeft = false) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = null, + senderName = "sender", + message = "message", + isGroupChat = false, + ) + ) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = BubbleBarFlyoutView(context, onLeft = true) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = null, + senderName = "sender", + message = "message", + isGroupChat = false, + ) + ) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = BubbleBarFlyoutView(context, onLeft = true) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = null, + senderName = "sender", + message = "really, really, really, really, really long message. like really.", + isGroupChat = false, + ) + ) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity -> + activity.actionBar?.hide() + val flyout = BubbleBarFlyoutView(context, onLeft = false) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = ColorDrawable(Color.RED), + senderName = "sender", + message = "message", + isGroupChat = true, + ) + ) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = BubbleBarFlyoutView(context, onLeft = true) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = ColorDrawable(Color.RED), + senderName = "sender", + message = "message", + isGroupChat = true, + ) + ) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = BubbleBarFlyoutView(context, onLeft = true) + flyout.setData( + BubbleBarFlyoutMessage( + senderAvatar = ColorDrawable(Color.RED), + senderName = "sender", + message = "really, really, really, really, really long message. like really.", + isGroupChat = true, + ) + ) + flyout + } + } +} 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 80% rename from quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt index a5327628d1..0005df6a6e 100644 --- a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt @@ -1,3 +1,18 @@ +/* + * 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 @@ -19,7 +34,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]. */ @@ -57,25 +72,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 +98,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 92% rename from quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt index 5c7b4aba4d..4ea74df776 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt @@ -30,11 +30,12 @@ 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.WidgetUtils.createAppWidgetProviderInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo import com.google.common.truth.Truth.assertThat @@ -62,7 +63,7 @@ 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 @@ -93,9 +94,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 +104,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) @@ -156,7 +157,7 @@ class WidgetsPredictionsRequesterTest { } @Test - fun filterPredictions_appPredictions_returnsWidgetFromPackage() { + fun filterPredictions_appPredictions_returnsEmptyList() { val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) val filter: Predicate = notOnUiSurfaceFilter(widgetsAlreadyOnSurface) @@ -176,8 +177,7 @@ class WidgetsPredictionsRequesterTest { ), ) - assertThat(filterPredictions(predictions, allWidgets, filter)) - .containsExactly(widgetItem1a, widgetItem2) + assertThat(filterPredictions(predictions, allWidgets, filter)).isEmpty() } private fun createWidgetItem( @@ -214,7 +214,7 @@ class WidgetsPredictionsRequesterTest { .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/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt new file mode 100644 index 0000000000..a57fb702d6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation + +object TaskbarControllerTestUtil { + inline fun runOnMainSync(crossinline runTest: () -> Unit) { + getInstrumentation().runOnMainSync { runTest() } + } +} 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 0000000000..72bbfc9496 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.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.launcher3.taskbar + +import androidx.test.platform.app.InstrumentationRegistry +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 { + + private val context = + TaskbarWindowSandboxContext.create( + InstrumentationRegistry.getInstrumentation().targetContext + ) + + @get:Rule(order = 0) 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 0000000000..961d4dc7c9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt @@ -0,0 +1,216 @@ +/* + * 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.test + +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TOOLTIP_STEP_FEATURES +import com.android.launcher3.taskbar.TOOLTIP_STEP_NONE +import com.android.launcher3.taskbar.TOOLTIP_STEP_PINNING +import com.android.launcher3.taskbar.TOOLTIP_STEP_SWIPE +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarEduTooltipController +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.TaskbarPinningPreferenceRule +import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule +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.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +@Ignore +class TaskbarEduTooltipControllerTest { + + private val context = + TaskbarWindowSandboxContext.create( + InstrumentationRegistry.getInstrumentation().targetContext + ) + + @get:Rule(order = 0) + val tooltipStepPreferenceRule = + TaskbarPreferenceRule( + context, + OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem, + ) + + @get:Rule(order = 1) + val searchEduPreferenceRule = + TaskbarPreferenceRule( + context, + OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN, + ) + + @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context) + + @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private val wasInTestHarness = Utilities.isRunningInTestHarness() + + @Before + fun setUp() { + Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started") + Utilities.disableRunningInTestHarnessForTests() + } + + @After + fun tearDown() { + if (wasInTestHarness) { + Utilities.enableRunningInTestHarnessForTests() + } + Log.e("Taskbar", "TaskbarEduTooltipControllerTest test completed") + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() { + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = 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. + tooltipStepPreferenceRule.value = 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() { + tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() { + tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHide_whenTooltipIsOpen_shouldCloseTooltip() { + tooltipStepPreferenceRule.value = 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() { + searchEduPreferenceRule.value = 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/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 0f06d98740..02d62186b1 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; @@ -18,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -26,15 +29,18 @@ import static org.mockito.Mockito.when; import android.os.Handler; import android.view.View; +import android.view.inputmethod.Flags; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; 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.systemui.contextualeducation.GestureType; import org.junit.Before; import org.junit.Test; @@ -49,6 +55,10 @@ public class TaskbarNavButtonControllerTest { @Mock SystemUiProxy mockSystemUiProxy; + + @Mock + ContextualEduStatsManager mockContextualEduStatsManager; + @Mock TouchInteractionService mockService; @Mock @@ -97,6 +107,7 @@ public class TaskbarNavButtonControllerTest { mockService, mCallbacks, mockSystemUiProxy, + mockContextualEduStatsManager, mockHandler, mockAssistUtils); } @@ -107,10 +118,36 @@ public class TaskbarNavButtonControllerTest { verify(mockSystemUiProxy, times(1)).onBackPressed(); } + @Test + public void testPressBack_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_BACK, mockView); + verify(mockContextualEduStatsManager, times(1)) + .updateEduStats(/* 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 @@ -172,12 +209,26 @@ public class TaskbarNavButtonControllerTest { assertThat(mHomePressCount).isEqualTo(1); } + @Test + public void testPressHome_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockContextualEduStatsManager, times(1)) + .updateEduStats(/* 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(mockContextualEduStatsManager, times(1)) + .updateEduStats(/* isTrackpad= */ eq(false), eq(GestureType.OVERVIEW)); + } + @Test public void testPressRecentsWithScreenPinned_noNavigationToOverview() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); 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 0000000000..4aac1dc83f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt @@ -0,0 +1,233 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +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 { + + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + @get:Rule 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/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt index c09dcf27de..43d924a259 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt @@ -16,22 +16,25 @@ package com.android.launcher3.taskbar.allapps +import android.animation.AnimatorTestRule import android.content.ComponentName import android.content.Intent import android.os.Process -import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.BubbleTextView import com.android.launcher3.appprediction.PredictionRowView import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.notification.NotificationKeyData -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.overlay.TaskbarOverlayController +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.PackageUserKey +import com.android.launcher3.util.TestUtil import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -41,89 +44,102 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) class TaskbarAllAppsControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule() + @get:Rule + val taskbarUnitTestRule = + TaskbarUnitTestRule( + this, + TaskbarWindowSandboxContext.create(getInstrumentation().targetContext), + ) + @get:Rule val animatorTestRule = AnimatorTestRule(this) @InjectController lateinit var allAppsController: TaskbarAllAppsController @InjectController lateinit var overlayController: TaskbarOverlayController @Test - @UiThreadTest fun testToggle_once_showsAllApps() { - allAppsController.toggle() + runOnMainSync { allAppsController.toggle() } assertThat(allAppsController.isOpen).isTrue() } @Test - @UiThreadTest fun testToggle_twice_closesAllApps() { - allAppsController.toggle() - allAppsController.toggle() + runOnMainSync { + allAppsController.toggle() + allAppsController.toggle() + } assertThat(allAppsController.isOpen).isFalse() } @Test - @UiThreadTest fun testToggle_taskbarRecreated_allAppsReopened() { - allAppsController.toggle() + runOnMainSync { allAppsController.toggle() } taskbarUnitTestRule.recreateTaskbar() assertThat(allAppsController.isOpen).isTrue() } @Test - @UiThreadTest fun testSetApps_beforeOpened_cachesInfo() { - allAppsController.setApps(TEST_APPS, 0, emptyMap()) - allAppsController.toggle() + val overlayContext = + TestUtil.getOnUiThread { + allAppsController.setApps(TEST_APPS, 0, emptyMap()) + allAppsController.toggle() + overlayController.requestWindow() + } - val overlayContext = overlayController.requestWindow() assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS) } @Test - @UiThreadTest fun testSetApps_afterOpened_updatesStore() { - allAppsController.toggle() - allAppsController.setApps(TEST_APPS, 0, emptyMap()) + val overlayContext = + TestUtil.getOnUiThread { + allAppsController.toggle() + allAppsController.setApps(TEST_APPS, 0, emptyMap()) + overlayController.requestWindow() + } - val overlayContext = overlayController.requestWindow() assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS) } @Test - @UiThreadTest fun testSetPredictedApps_beforeOpened_cachesInfo() { - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - allAppsController.toggle() - val predictedApps = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - .predictedApps + TestUtil.getOnUiThread { + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + allAppsController.toggle() + + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .predictedApps + } + assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS) } @Test - @UiThreadTest fun testSetPredictedApps_afterOpened_cachesInfo() { - allAppsController.toggle() - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - val predictedApps = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - .predictedApps + TestUtil.getOnUiThread { + allAppsController.toggle() + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .predictedApps + } + assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS) } @Test fun testUpdateNotificationDots_appInfo_hasDot() { - getInstrumentation().runOnMainSync { + runOnMainSync { allAppsController.setApps(TEST_APPS, 0, emptyMap()) allAppsController.toggle() taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted( @@ -133,39 +149,56 @@ class TaskbarAllAppsControllerTest { } // Ensure the recycler view fully inflates before trying to grab an icon. - getInstrumentation().runOnMainSync { - val btv = + val btv = + TestUtil.getOnUiThread { overlayController .requestWindow() .appsView .activeRecyclerView .findViewHolderForAdapterPosition(0) ?.itemView as? BubbleTextView - assertThat(btv?.hasDot()).isTrue() - } + } + assertThat(btv?.hasDot()).isTrue() } @Test - @UiThreadTest fun testUpdateNotificationDots_predictedApp_hasDot() { - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - allAppsController.toggle() + runOnMainSync { + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + allAppsController.toggle() + taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted( + PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]), + NotificationKeyData("key"), + ) + } - taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted( - PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]), - NotificationKeyData("key"), - ) - - val predictionRowView = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - val btv = predictionRowView.getChildAt(0) as BubbleTextView + val btv = + TestUtil.getOnUiThread { + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .getChildAt(0) as BubbleTextView + } assertThat(btv.hasDot()).isTrue() } + @Test + fun testToggleSearch_searchEditTextFocused() { + runOnMainSync { allAppsController.toggleSearch() } + runOnMainSync { + // All Apps is now attached to window. Open animation is posted but not started. + } + + runOnMainSync { + // Animation has started. Advance to end of animation. + animatorTestRule.advanceTimeBy(overlayController.openDuration.toLong()) + } + val editText = overlayController.requestWindow().appsView.searchUiManager.editText + assertThat(editText?.hasFocus()).isTrue() + } + private companion object { private val TEST_APPS = Array(16) { diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt new file mode 100644 index 0000000000..785ec66420 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt @@ -0,0 +1,156 @@ +/* + * 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.view.MotionEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.quickstep.inputconsumers.BubbleBarInputConsumer +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +/** + * Tests for bubble bar input consumer, namely the static method that indicates whether the input + * consumer should handle the event. + */ +@RunWith(AndroidJUnit4::class) +class BubbleBarInputConsumerTest { + + private lateinit var bubbleControllers: BubbleControllers + + @Mock private lateinit var taskbarActivityContext: TaskbarActivityContext + @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 + + @Mock private lateinit var motionEvent: MotionEvent + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + bubbleControllers = + BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + bubbleCreator + ) + } + + @Test + fun testIsEventOnBubbles_noTaskbarActivityContext() { + assertThat(BubbleBarInputConsumer.isEventOnBubbles(null, motionEvent)).isFalse() + } + + @Test + fun testIsEventOnBubbles_bubblesNotEnabled() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(false) + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isFalse() + } + + @Test + fun testIsEventOnBubbles_noBubbleControllers() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(null) + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isFalse() + } + + @Test + fun testIsEventOnBubbles_noBubbles() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers) + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isFalse() + } + + @Test + fun testIsEventOnBubbles_eventOnStashedHandle() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers) + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + whenever(bubbleStashController.isStashed).thenReturn(true) + whenever(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(true) + + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isTrue() + } + + @Test + fun testIsEventOnBubbles_eventNotOnStashedHandle() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers) + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + whenever(bubbleStashController.isStashed).thenReturn(true) + whenever(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(false) + + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isFalse() + } + + @Test + fun testIsEventOnBubbles_eventOnVisibleBubbleView() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers) + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleBarViewController.isBubbleBarVisible).thenReturn(true) + whenever(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(true) + + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isTrue() + } + + @Test + fun testIsEventOnBubbles_eventNotOnVisibleBubbleView() { + whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true) + whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers) + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleBarViewController.isBubbleBarVisible).thenReturn(true) + whenever(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(false) + + assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent)) + .isFalse() + } +} 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 0000000000..94f9cf5398 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.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.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) + bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + bubble = + BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "") + 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 0000000000..d5a76a2f4c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt @@ -0,0 +1,116 @@ +/* + * 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) + } + + 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, + 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, + removedBubbleIndex = 2, + 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 index cc579abc9e..84e872da14 100644 --- 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 @@ -24,6 +24,7 @@ import android.view.LayoutInflater import android.view.View import android.view.View.VISIBLE import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule import androidx.core.graphics.drawable.toBitmap import androidx.dynamicanimation.animation.DynamicAnimation import androidx.test.core.app.ApplicationProvider @@ -34,13 +35,14 @@ 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.launcher3.taskbar.bubbles.stashing.BubbleStashController import com.android.wm.shell.shared.animation.PhysicsAnimator import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleInfo 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 @@ -53,6 +55,8 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class BubbleBarViewAnimatorTest { + @get:Rule val animatorTestRule = AnimatorTestRule() + private val context = ApplicationProvider.getApplicationContext() private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler private lateinit var overflowView: BubbleView @@ -60,6 +64,7 @@ class BubbleBarViewAnimatorTest { private lateinit var bubble: BubbleBarBubble private lateinit var bubbleBarView: BubbleBarView private lateinit var bubbleStashController: BubbleStashController + private val onExpandedNoOp = Runnable {} @Before fun setUp() { @@ -74,13 +79,18 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -94,7 +104,7 @@ class BubbleBarViewAnimatorTest { assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).isTrue() // execute the hide bubble animation assertThat(animatorScheduler.delayedBlock).isNotNull() @@ -107,7 +117,7 @@ class BubbleBarViewAnimatorTest { assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() verify(bubbleStashController).stashBubbleBarImmediate() } @@ -118,13 +128,18 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -138,7 +153,7 @@ class BubbleBarViewAnimatorTest { assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).isTrue() verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion() @@ -151,7 +166,7 @@ class BubbleBarViewAnimatorTest { assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() } @Test @@ -161,13 +176,18 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start @@ -175,7 +195,7 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() @@ -185,7 +205,7 @@ class BubbleBarViewAnimatorTest { // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait @@ -201,13 +221,18 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete @@ -226,7 +251,7 @@ class BubbleBarViewAnimatorTest { animator.onStashStateChangingWhileAnimating() } - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait @@ -242,13 +267,18 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) + animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start @@ -256,15 +286,156 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).isTrue() assertThat(animatorScheduler.delayedBlock).isNotNull() handleAnimator.cancel() handleAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() assertThat(animatorScheduler.delayedBlock).isNull() } + @Test + fun animateBubbleInForStashed_autoExpanding() { + setUpBubbleBar() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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() + + verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleInForStashed_expandedWhileAnimatingIn() { + setUpBubbleBar() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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() + setUpBubbleStashController() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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() + // 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() + + 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() @@ -274,12 +445,17 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) @@ -289,7 +465,7 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) @@ -300,7 +476,7 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) InstrumentationRegistry.getInstrumentation().waitForIdleSync() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() + assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(handle.translationY).isEqualTo(0) assertThat(handle.alpha).isEqualTo(1) @@ -317,12 +493,19 @@ class BubbleBarViewAnimatorTest { val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) @@ -332,18 +515,13 @@ class BubbleBarViewAnimatorTest { 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(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animatorScheduler.delayedBlock).isNull() verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() } @Test @@ -356,7 +534,12 @@ class BubbleBarViewAnimatorTest { val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + animatorScheduler + ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) @@ -366,20 +549,314 @@ class BubbleBarViewAnimatorTest { PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() + assertThat(animator.isAnimating).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(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) verify(bubbleStashController).showBubbleBarImmediate() } + @Test + fun animateToInitialState_expandedWhileAnimatingIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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() + verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_expandedWhileFullyIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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(animator.isAnimating).isTrue() + // 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() + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpandedNoOp, + 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(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + assertThat(animator.isAnimating).isFalse() + // the bubble bar translation y should be back to its initial value + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + verify(bubbleStashController).showBubbleBarImmediate() + } + + @Test + fun animateBubbleBarForCollapsed_autoExpanding() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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 {} + barAnimator.assertIsRunning() + 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(bubbleBarView.isExpanded).isTrue() + verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true } + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(animator.isAnimating).isTrue() + + 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(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileFullyIn() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + onExpanded, + 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() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // 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() + verify(bubbleStashController).showBubbleBarImmediate() + assertThat(notifiedExpanded).isTrue() + } + private fun setUpBubbleBar() { bubbleBarView = BubbleBarView(context) InstrumentationRegistry.getInstrumentation().runOnMainSync { @@ -393,7 +870,7 @@ class BubbleBarViewAnimatorTest { bubbleBarView.addView(overflowView) val bubbleInfo = - BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false) + BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false, true) bubbleView = inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView bubble = @@ -407,14 +884,22 @@ class BubbleBarViewAnimatorTest { private fun setUpBubbleStashController() { bubbleStashController = mock() whenever(bubbleStashController.isStashed).thenReturn(true) - whenever(bubbleStashController.diffBetweenHandleAndBarCenters) + whenever(bubbleStashController.getDiffBetweenHandleAndBarCenters()) .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS) - whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation) + whenever(bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()) .thenReturn(HANDLE_TRANSLATION) whenever(bubbleStashController.bubbleBarTranslationYForTaskbar) .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) } + 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 PhysicsAnimator.assertIsRunning() { InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(isRunning()).isTrue() 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 0000000000..a58ce08209 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.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.flyout + +import android.content.Context +import android.view.Gravity +import android.widget.FrameLayout +import android.widget.TextView +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [BubbleBarFlyoutController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarFlyoutControllerTest { + + private lateinit var flyoutController: BubbleBarFlyoutController + private lateinit var flyoutContainer: FrameLayout + private val context = ApplicationProvider.getApplicationContext() + private val flyoutMessage = + BubbleBarFlyoutMessage(senderAvatar = null, "sender name", "message", isGroupChat = false) + private var onLeft = true + + @Before + fun setUp() { + flyoutContainer = FrameLayout(context) + val positioner = + object : BubbleBarFlyoutPositioner { + override val isOnLeft: Boolean + get() = onLeft + + override val targetTy: Float + get() = 50f + } + flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner) + } + + @Test + fun flyoutPosition_left() { + flyoutController.setUpFlyout(flyoutMessage) + 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 + flyoutController.setUpFlyout(flyoutMessage) + 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() { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val sender = flyout.findViewById(R.id.bubble_flyout_name) + assertThat(sender.text).isEqualTo("sender name") + val message = flyout.findViewById(R.id.bubble_flyout_text) + assertThat(message.text).isEqualTo("message") + } + + @Test + fun hideFlyout_removedFromContainer() { + flyoutController.setUpFlyout(flyoutMessage) + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.hideFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(0) + } +} 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 0000000000..4106a2c9a6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.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.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.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.clearInvocations +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_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.init( + taskbarInsetsController, + bubbleBarViewController, + null, + ImmediateAction() + ) + } + + @Test + fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.isBubblesShowingOnHome = true + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.isBubblesShowingOnHome = false + } + + // 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.isBubblesShowingOnHome = true + } + + // 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.isBubblesShowingOnOverview = true + clearInvocations(bubbleBarViewController) + + // When switch out of the overview screen + persistentTaskBarStashController.isBubblesShowingOnOverview = false + + // Then bubble bar controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() { + // When switch to the overview screen + persistentTaskBarStashController.isBubblesShowingOnOverview = true + + // 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.isBubblesShowingOnOverview = true + 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.isBubblesShowingOnHome = true + + // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(HOTSEAT_TRANSLATION_Y) + + // Give bubble bar is not on home + persistentTaskBarStashController.isBubblesShowingOnHome = false + + // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + 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 0000000000..0f8a2c3eb0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt @@ -0,0 +1,43 @@ +/* + * 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, + private val hotseatBottomSpace: Int = HOTSEAT_BOTTOM_SPACE, + private val hotseatHeight: Int = HOTSEAT_HEIGHT +) : BubbleStashController.TaskbarHotseatDimensionsProvider { + override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace + + override fun getTaskbarHeight(): Int = taskBarHeight + + override fun getHotseatBottomSpace(): Int = hotseatBottomSpace + + override fun getHotseatHeight(): Int = hotseatHeight + + companion object { + const val TASKBAR_BOTTOM_SPACE = 0 + const val TASKBAR_HEIGHT = 110 + const val HOTSEAT_BOTTOM_SPACE = 20 + const val HOTSEAT_HEIGHT = 150 + } +} 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 0000000000..d4a3b3aee9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt @@ -0,0 +1,362 @@ +/* + * 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.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.util.MultiValueAlpha +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.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.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 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 + 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.init( + taskbarInsetsController, + bubbleBarViewController, + bubbleStashedHandleViewController, + ImmediateAction(), + ) + } + + @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.isBubblesShowingOnHome = true + } + + // 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 home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.isBubblesShowingOnOverview = true + } + + // 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 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 isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() { + // Given screen is locked and bubble bar has bubbles + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.isSysuiLocked = true + mTransientBubbleStashController.isBubblesShowingOnOverview = true + 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() + } + + @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() + } + + @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) + } + + 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 eebd8f9f16..4fa821db66 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,7 +18,6 @@ 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 @@ -26,11 +25,13 @@ 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,185 +42,182 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023"]) class TaskbarOverlayControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule() + @get:Rule + val taskbarUnitTestRule = + TaskbarUnitTestRule( + this, + TaskbarWindowSandboxContext.create(getInstrumentation().targetContext), + ) @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() } 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 0000000000..c48947e078 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt @@ -0,0 +1,87 @@ +/* + * 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.MainThreadInitializedObject +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 [MainThreadInitializedObject] 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 { + context.applicationContext.putObject( + DisplayController.INSTANCE, + object : DisplayController(context) { + override fun getInfo(): Info { + return spy(super.getInfo()) { + 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 0000000000..f7e457613a --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +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 { + + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + + @get:Rule 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/TaskbarPinningPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt new file mode 100644 index 0000000000..d417790d84 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.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.launcher3.taskbar.rules + +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.Flags.FLAG_ENABLE_TASKBAR_PINNING +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE +import com.android.launcher3.util.DisplayController +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Rule that allows modifying the Taskbar pinned preferences. + * + * The original preference values are restored on teardown. + * + * If this rule is being used with [TaskbarUnitTestRule], make sure this rule is applied first. + * + * This rule is overkill if a test does not need to change the mode during Taskbar's lifecycle. If + * the mode is static, use [TaskbarModeRule] instead, which forces the mode. A test can class can + * declare both this rule and [TaskbarModeRule] but using both for a test method is unsupported. + */ +class TaskbarPinningPreferenceRule(context: TaskbarWindowSandboxContext) : TestRule { + + private val setFlagsRule = + SetFlagsRule(FlagsParameterization(mapOf(FLAG_ENABLE_TASKBAR_PINNING to true))) + private val pinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) + private val desktopPinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING_IN_DESKTOP_MODE) + private val ruleChain = + RuleChain.outerRule(setFlagsRule).around(pinningRule).around(desktopPinningRule) + + var isPinned by pinningRule::value + var isPinnedInDesktopMode by desktopPinningRule::value + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + DisplayController.enableTaskbarModePreferenceForTests(true) + try { + ruleChain.apply(base, description).evaluate() + } finally { + DisplayController.enableTaskbarModePreferenceForTests(false) + } + } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt new file mode 100644 index 0000000000..a5154058fd --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.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.launcher3.taskbar.rules + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.window.WindowManagerProxy +import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy +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", "pixelTablet2023"]) +class TaskbarPinningPreferenceRuleTest { + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + + private val preferenceRule = TaskbarPinningPreferenceRule(context) + + @Test + fun testEnablePinning_verifyDisplayController() { + onSetup { + preferenceRule.isPinned = true + preferenceRule.isPinnedInDesktopMode = false + assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() + } + } + + @Test + fun testDisablePinning_verifyDisplayController() { + onSetup { + preferenceRule.isPinned = false + preferenceRule.isPinnedInDesktopMode = false + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + } + } + + @Test + fun testEnableDesktopPinning_verifyDisplayController() { + context.applicationContext.putObject( + WindowManagerProxy.INSTANCE, + TestWindowManagerProxy(context).apply { isInDesktopMode = true }, + ) + + onSetup { + preferenceRule.isPinned = false + preferenceRule.isPinnedInDesktopMode = true + assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() + } + } + + @Test + fun testDisableDesktopPinning_verifyDisplayController() { + context.applicationContext.putObject( + WindowManagerProxy.INSTANCE, + TestWindowManagerProxy(context).apply { isInDesktopMode = true }, + ) + + onSetup { + preferenceRule.isPinned = false + preferenceRule.isPinnedInDesktopMode = false + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + } + } + + @Test + fun testTearDown_afterTogglingPinnedPreference_preferenceReset() { + val wasPinned = preferenceRule.isPinned + onSetup { preferenceRule.isPinned = !preferenceRule.isPinned } + assertThat(preferenceRule.isPinned).isEqualTo(wasPinned) + } + + @Test + fun testTearDown_afterTogglingDesktopPreference_preferenceReset() { + val wasPinnedInDesktopMode = preferenceRule.isPinnedInDesktopMode + onSetup { preferenceRule.isPinnedInDesktopMode = !preferenceRule.isPinnedInDesktopMode } + assertThat(preferenceRule.isPinnedInDesktopMode).isEqualTo(wasPinnedInDesktopMode) + } + + /** Executes [runTest] after the [preferenceRule] setup phase completes. */ + private fun onSetup(runTest: () -> Unit) { + preferenceRule + .apply( + object : Statement() { + override fun evaluate() = runTest() + }, + DESCRIPTION, + ) + .evaluate() + } + + private companion object { + private val DESCRIPTION = + Description.createSuiteDescription(TaskbarPinningPreferenceRule::class.java) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt new file mode 100644 index 0000000000..a76a77d170 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt @@ -0,0 +1,54 @@ +/* + * 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.ConstantItem +import com.android.launcher3.LauncherPrefs +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Rule for modifying a Taskbar preference. + * + * The original preference value is restored on teardown. + */ +class TaskbarPreferenceRule( + context: TaskbarWindowSandboxContext, + private val constantItem: ConstantItem +) : TestRule { + + private val prefs = LauncherPrefs.get(context) + + var value: T + get() = prefs.get(constantItem) + set(value) = getInstrumentation().runOnMainSync { prefs.put(constantItem, value) } + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val originalValue = value + try { + base.evaluate() + } finally { + value = originalValue + } + } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt new file mode 100644 index 0000000000..46817d2bbd --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +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", "pixelTablet2023"]) +class TaskbarPreferenceRuleTest { + + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + private val preferenceRule = TaskbarPreferenceRule(context, TASKBAR_PINNING) + + @Test + fun testSetup_toggleBoolean_updatesPreferences() { + val originalValue = preferenceRule.value + onSetup { + preferenceRule.value = !preferenceRule.value + assertThat(preferenceRule.value).isNotEqualTo(originalValue) + } + } + + @Test + fun testTeardown_afterTogglingBoolean_preferenceReset() { + val originalValue = preferenceRule.value + onSetup { preferenceRule.value = !preferenceRule.value } + assertThat(preferenceRule.value).isEqualTo(originalValue) + } + + private fun onSetup(runTest: () -> Unit) { + preferenceRule + .apply( + object : Statement() { + override fun evaluate() = runTest() + }, + DESCRIPTION, + ) + .evaluate() + } + + private companion object { + private val DESCRIPTION = + Description.createSuiteDescription(TaskbarPreferenceRule::class.java) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt similarity index 50% rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt index a999e7f7de..cb5e464dde 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt @@ -14,41 +14,53 @@ * limitations under the License. */ -package com.android.launcher3.taskbar +package com.android.launcher3.taskbar.rules import android.app.Instrumentation import android.app.PendingIntent import android.content.IIntentSender import android.content.Intent +import android.provider.Settings +import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ServiceTestRule import com.android.launcher3.LauncherAppState +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarManager import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks +import com.android.launcher3.taskbar.TaskbarViewController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric +import com.android.launcher3.util.TestUtil 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.rules.RuleChain +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 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. + * `@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]. * - * 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() { @@ -58,12 +70,19 @@ import org.junit.runners.model.Statement * } * ``` */ -class TaskbarUnitTestRule : MethodRule { +class TaskbarUnitTestRule( + private val testInstance: Any, + private val context: TaskbarWindowSandboxContext, +) : TestRule { + private val instrumentation = InstrumentationRegistry.getInstrumentation() private val serviceTestRule = ServiceTestRule() + private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE) + private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE) + private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule) + private lateinit var taskbarManager: TaskbarManager - private lateinit var target: Any val activityContext: TaskbarActivityContext get() { @@ -71,18 +90,35 @@ class TaskbarUnitTestRule : MethodRule { ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") } - override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement { + override fun apply(base: Statement, description: Description): Statement { + return settingRules.apply(createStatement(base, description), description) + } + + private fun createStatement(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { - this@TaskbarUnitTestRule.target = target - val context = instrumentation.targetContext + // Only run test when Taskbar is enabled. instrumentation.runOnMainSync { assumeTrue( LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent ) } + // Process secure setting annotations. + instrumentation.runOnMainSync { + userSetupCompleteRule.putInt( + if (description.getAnnotation(UserSetupMode::class.java) != null) { + 0 + } else { + 1 + } + ) + kidsModeRule.putInt( + if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0 + ) + } + // Check for existing Taskbar instance from Launcher process. val launcherTaskbarManager: TaskbarManager? = if (!isRunningInRobolectric) { @@ -99,51 +135,58 @@ class TaskbarUnitTestRule : MethodRule { null } - instrumentation.runOnMainSync { - taskbarManager = - TaskbarManager( - context, - AllAppsActionManager(context, UI_HELPER_EXECUTOR) { - PendingIntent(IIntentSender.Default()) - }, - object : TaskbarNavButtonCallbacks {}, - ) - } + taskbarManager = + TestUtil.getOnUiThread { + object : + TaskbarManager( + context, + AllAppsActionManager(context, UI_HELPER_EXECUTOR) { + PendingIntent(IIntentSender.Default()) + }, + object : TaskbarNavButtonCallbacks {}, + DesktopVisibilityController(context), + ) { + override fun recreateTaskbar() { + super.recreateTaskbar() + if (currentActivityContext != null) injectControllers() + } + } + } try { + TaskbarViewController.enableModelLoadingForTests(false) + // Replace Launcher Taskbar window with test instance. instrumentation.runOnMainSync { - launcherTaskbarManager?.removeTaskbarRootViewFromWindow() + launcherTaskbarManager?.setSuspended(true) taskbarManager.onUserUnlocked() // Required to complete initialization. } - injectControllers() base.evaluate() } finally { // Revert Taskbar window. instrumentation.runOnMainSync { taskbarManager.destroy() - launcherTaskbarManager?.addTaskbarRootViewToWindow() + launcherTaskbarManager?.setSuspended(false) } + + TaskbarViewController.enableModelLoadingForTests(true) } } } } /** Simulates Taskbar recreation lifecycle. */ - fun recreateTaskbar() { - taskbarManager.recreateTaskbar() - injectControllers() - } + fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() } private fun injectControllers() { val controllers = activityContext.controllers val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } - target.javaClass.fields + testInstance.javaClass.fields .filter { it.isAnnotationPresent(InjectController::class.java) } .forEach { it.set( - target, + testInstance, controllerFieldsByType[it.type]?.get(controllers) ?: throw NoSuchElementException("Failed to find controller for ${it.type}"), ) @@ -161,4 +204,35 @@ class TaskbarUnitTestRule : MethodRule { @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 + + /** Rule for Taskbar integer-based secure settings. */ + private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule { + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val originalValue = + Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0) + try { + base.evaluate() + } finally { + instrumentation.runOnMainSync { putInt(originalValue) } + } + } + } + } + + /** Puts [value] into secure settings under [settingName]. */ + fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value) + } } 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 0000000000..5d4fdc5418 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt @@ -0,0 +1,184 @@ +/* + * 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.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarKeyguardController +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.taskbar.TaskbarStashController +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.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +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 { + + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + + @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) + } + } + + @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() + } + } + + /** + * 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 0000000000..ee21df8f25 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.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.taskbar.rules + +import android.content.Context +import android.content.ContextWrapper +import android.os.Bundle +import android.view.Display +import com.android.launcher3.util.MainThreadInitializedObject +import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext + +/** + * Sandbox wrapper where [createWindowContext] provides contexts that are still sandboxed within + * [application]. + * + * Taskbar can create window contexts, which need to operate under the same sandbox application, but + * [Context.getApplicationContext] by default returns the actual application. For this reason, + * [SandboxContext] overrides [getApplicationContext] to return itself, which prevents leaving the + * sandbox. [SandboxContext] and the real application have different sets of + * [MainThreadInitializedObject] instances, so overriding the application prevents the latter set + * from leaking into the sandbox. Similarly, this implementation overrides [getApplicationContext] + * to return the original sandboxed [application], and it wraps created windowed contexts to + * propagate this [application]. + */ +class TaskbarWindowSandboxContext +private constructor(private val application: SandboxContext, base: Context) : ContextWrapper(base) { + + override fun createWindowContext(type: Int, options: Bundle?): Context { + return TaskbarWindowSandboxContext(application, super.createWindowContext(type, options)) + } + + override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context { + return TaskbarWindowSandboxContext( + application, + super.createWindowContext(display, type, options), + ) + } + + override fun getApplicationContext(): SandboxContext = application + + companion object { + /** Creates a [TaskbarWindowSandboxContext] to sandbox [base] for Taskbar tests. */ + fun create(base: Context): TaskbarWindowSandboxContext { + return SandboxContext(base).let { TaskbarWindowSandboxContext(it, it) } + } + } +} 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 0000000000..4834d48973 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarWindowSandboxContextTest { + + private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext) + + @Test + fun testCreateWindowContext_applicationContextSandboxed() { + val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null) + assertThat(windowContext.applicationContext).isInstanceOf(SandboxContext::class.java) + } + + @Test + fun testCreateWindowContext_nested_applicationContextSandboxed() { + val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null) + val nestedContext = windowContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null) + assertThat(nestedContext.applicationContext).isInstanceOf(SandboxContext::class.java) + } +} 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 0000000000..2a0aa4ccb1 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java @@ -0,0 +1,276 @@ +/* + * 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.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.StatefulActivity; +import com.android.launcher3.util.SystemUiController; +import com.android.quickstep.util.ActivityInitListener; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.system.InputConsumerController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +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< + RECENTS_CONTAINER extends Context & RecentsViewContainer, + STATE extends BaseState, + RECENTS_VIEW extends RecentsView, + ACTIVITY_TYPE extends StatefulActivity & RecentsViewContainer, + ACTIVITY_INTERFACE extends BaseActivityInterface, + SWIPE_HANDLER extends AbsSwipeUpHandler> { + + protected final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + protected final TaskAnimationManager mTaskAnimationManager = new TaskAnimationManager(mContext); + protected final RecentsAnimationDeviceState mRecentsAnimationDeviceState = + new RecentsAnimationDeviceState(mContext, true); + 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)); + 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 final RecentsAnimationTargets mRecentsAnimationTargets = new RecentsAnimationTargets( + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + /* homeContentInsets= */ new Rect(), + /* minimizedHomeBounds= */ null, + new Bundle()); + + @Mock protected ACTIVITY_INTERFACE mActivityInterface; + @Mock protected ActivityInitListener mActivityInitListener; + @Mock protected RecentsAnimationController mRecentsAnimationController; + @Mock protected STATE mState; + @Mock protected ViewTreeObserver mViewTreeObserver; + @Mock protected DragLayer mDragLayer; + @Mock protected LauncherRootView mRootView; + @Mock protected SystemUiController mSystemUiController; + @Mock protected GestureState mGestureState; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @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() { + 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(mActivityInitListener); + 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(mActivityInitListener).register(eq(reasonString)); + } + + @Test + public void testOnRecentsAnimationCanceled_unregistersActivityInitListener() { + createSwipeHandler() + .onRecentsAnimationCanceled(new HashMap<>()); + + runOnMainSync(() -> verify(mActivityInitListener) + .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled"))); + } + + @Test + public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() { + createSwipeHandler().onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mActivityInitListener) + .unregister("AbsSwipeUpHandler.invalidateHandler")); + } + + @Test + public void testOnConsumerAboutToBeSwitched_midQuickSwitch_unregistersActivityInitListener() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK) + .onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mActivityInitListener) + .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation"))); + } + + @Test + public void testStartNewTask_finishesRecentsAnimationController() { + SWIPE_HANDLER absSwipeUpHandler = createSwipeHandler(); + + onRecentsAnimationStart(absSwipeUpHandler); + + runOnMainSync(() -> { + absSwipeUpHandler.startNewTask(unused -> {}); + verify(mRecentsAnimationController).finish(anyBoolean(), any()); + }); + } + + @Test + public void testHomeGesture_finishesRecentsAnimationController() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).detachNavigationBarFromApp(true); + verify(mRecentsAnimationController).finish(anyBoolean(), any(), anyBoolean()); + }); + } + + 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)); + swipeHandler.onCalculateEndTarget(); + runOnMainSync(swipeHandler::onSettledOnEndTarget); + + return swipeHandler; + } + + private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) { + when(mActivityInterface.getOverviewWindowBounds(any(), any())).thenReturn(new Rect()); + doNothing().when(mActivityInterface).setOnDeferredActivityLaunchCallback(any()); + + runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart( + mRecentsAnimationController, mRecentsAnimationTargets)); + } + + private 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/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java new file mode 100644 index 0000000000..dd0b4b30d0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.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; + +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< + RecentsActivity, + RecentsState, + FallbackRecentsView, + RecentsActivity, + FallbackActivityInterface, + FallbackSwipeHandler> { + + @Mock private RecentsActivity mRecentsActivity; + @Mock private FallbackRecentsView mRecentsView; + + + @Override + protected FallbackSwipeHandler createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new FallbackSwipeHandler( + mContext, + mRecentsAnimationDeviceState, + mTaskAnimationManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController); + } + + @Override + protected RecentsActivity getRecentsContainer() { + return mRecentsActivity; + } + + @Override + protected FallbackRecentsView getRecentsView() { + return mRecentsView; + } +} 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 0000000000..24f9696437 --- /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 0000000000..1f88743364 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt @@ -0,0 +1,99 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.R +import com.android.launcher3.util.LauncherModelHelper +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.shared.system.InputConsumerController +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 + + 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) + + @Before + fun setup() { + sandboxContext.putObject(SystemUiProxy.INSTANCE, systemUiProxy) + val deviceState = mock(RecentsAnimationDeviceState::class.java) + whenever(deviceState.rotationTouchHelper).thenReturn(mock(RotationTouchHelper::class.java)) + gestureState = spy(GestureState(OverviewComponentObserver(sandboxContext, deviceState), 0)) + + underTest = + LauncherSwipeHandlerV2( + sandboxContext, + deviceState, + taskAnimationManager, + gestureState, + 0, + false, + inputConsumerController + ) + underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false) + } + + @Test + fun goHomeFromAppByTrackpad_updateEduStats() { + gestureState.setTrackpadGestureType(GestureState.TrackpadGestureType.THREE_FINGER) + underTest.onGestureEnded(flingSpeed, PointF()) + verify(systemUiProxy) + .updateContextualEduStats( + /* isTrackpadGesture= */ eq(true), + eq(GestureType.HOME.toString()) + ) + } + + @Test + fun goHomeFromAppByTouch_updateEduStats() { + underTest.onGestureEnded(flingSpeed, PointF()) + verify(systemUiProxy) + .updateContextualEduStats( + /* isTrackpadGesture= */ eq(false), + eq(GestureType.HOME.toString()) + ) + } +} 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 0000000000..653dc01306 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java @@ -0,0 +1,92 @@ +/* + * 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.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.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase< + QuickstepLauncher, + LauncherState, + RecentsView, + QuickstepLauncher, + LauncherActivityInterface, + LauncherSwipeHandlerV2> { + + @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); + } + + @Override + protected LauncherSwipeHandlerV2 createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new LauncherSwipeHandlerV2( + mContext, + mRecentsAnimationDeviceState, + mTaskAnimationManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController); + } + + @Override + protected QuickstepLauncher getRecentsContainer() { + return mQuickstepLauncher; + } + + @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 0000000000..0ff142a47b --- /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 0000000000..0ae710f866 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -0,0 +1,179 @@ +/* + * 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 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.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.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.mock + +@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() + + @Suppress("UNCHECKED_CAST") + @Before + fun setup() { + setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT) + + sut = + spy( + OverviewCommandHelper( + touchInteractionService = mock(), + overviewComponentObserver = mock(), + taskAnimationManager = mock(), + dispatcherProvider = TestDispatcherProvider(dispatcher) + ) + ) + + doAnswer { invocation -> + val pendingCallback = invocation.arguments[1] as () -> Unit + + val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull() + if (delayInMillis != null) { + runBlocking { + testScope.backgroundScope.launch { + delay(delayInMillis) + pendingCallback.invoke() + } + } + } + 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) + } + + private companion object { + const val QUEUE_TIMEOUT = 5001L + } +} 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 0000000000..c18f60476b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java @@ -0,0 +1,452 @@ +/* + * 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.util.Executors.MAIN_EXECUTOR; +import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_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.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.util.DisplayController; +import com.android.launcher3.util.MainThreadInitializedObject.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 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; + @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; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mTopTaskTracker.getCachedTopTask(anyBoolean())).thenReturn(mTaskInfo); + when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP); + when(mDelegate.allowInterceptByParent()).thenReturn(true); + MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable); + mLongPressTriggered.set(false); + when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable); + initializeObjectUnderTest(); + } + + @After + public void tearDown() { + 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()); + } + + @Test + public void testDelegateDisallowsTouchInterceptAfterTouchDown() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + + // 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()); + + 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()); + } + + @Test + public void testLongPressTriggered() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + } + + @Test + public void testLongPressTriggeredWithSlightVerticalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP - 1))); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + } + + @Test + public void testLongPressTriggeredWithSlightHorizontalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP - 1), 0)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + } + + @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. + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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); + SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + } 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. + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressAbortedByTouchUp() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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. + SystemClock.sleep(20); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + } + + @Test + public void testLongPressAbortedByTouchCancel() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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. + SystemClock.sleep(20); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedVertically() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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. + SystemClock.sleep(20); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedHorizontally() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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. + SystemClock.sleep(20); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + } + + @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. + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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); + SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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()); + } 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. + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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); + SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + 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()); + } 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)); + SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // 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()); + } + + @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()); + } + + private void initializeObjectUnderTest() { + if (mContext != null) { + mContext.onDestroy(); + } + mContext = new SandboxContext(getApplicationContext()); + mContext.putObject(TopTaskTracker.INSTANCE, mTopTaskTracker); + mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x; + mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor, + mDeviceState, mNavHandle, mGestureState); + mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler); + } + + /** 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 static MotionEvent generateMotionEvent(int motionAction, float x, float y) { + return MotionEvent.obtain(0, 0, motionAction, x, y, 0); + } + + private static AutoCloseable overrideTwoStageFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_LPNH_TWO_STAGES", + value, + () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages()); + } +} 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 0000000000..0a607744d0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt @@ -0,0 +1,153 @@ +/* + * 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.LauncherPrefs.Companion.THEMED_ICONS +import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY +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.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 + + @Captor private lateinit var mEventCaptor: ArgumentCaptor + @Mock private lateinit var mTracker: DaggerSingletonTracker + + private var mDefaultThemedIcons = false + private var mDefaultAllowRotation = false + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(mStatsLogManager.logger()).doReturn(mMockLogger) + whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger) + mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS) + mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION) + // To match the default value of THEMED_ICONS + LauncherPrefs.get(mContext).put(THEMED_ICONS, false) + // To match the default value of ALLOW_ROTATION + LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false) + + mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker) + } + + @After + fun tearDown() { + LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons) + LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation) + } + + @Test + fun loggingPrefs_correctDefaultValue() { + val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker) + + 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).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/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..7d09efd100 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.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.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback + +class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier { + val listeners = mutableListOf() + + override fun addCallback(callback: HighResLoadingStateChangedCallback) { + listeners.add(callback) + } + + override fun removeCallback(callback: HighResLoadingStateChangedCallback) { + listeners.remove(callback) + } +} 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 0000000000..fc2f029419 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository { + private var recentsDeviceProfile = + RecentsDeviceProfile( + isLargeScreen = 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 0000000000..c328672d9d --- /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 0000000000..5de876a525 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt @@ -0,0 +1,74 @@ +/* + * 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.launcher3.util.CancellableTask +import com.android.quickstep.TaskIconCache +import com.android.quickstep.task.thumbnail.data.TaskIconDataSource +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FakeTaskIconDataSource : TaskIconDataSource { + + val taskIdToDrawable: MutableMap = + (0..10).associateWith { mockCopyableDrawable() }.toMutableMap() + + val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() + var shouldLoadSynchronously: Boolean = true + + /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */ + override fun getIconInBackground( + task: Task, + callback: TaskIconCache.GetTaskIconCallback + ): CancellableTask<*>? { + val wrappedCallback = { + callback.onTaskIconReceived( + taskIdToDrawable.getValue(task.key.id), + "content desc ${task.key.id}", + "title ${task.key.id}" + ) + } + if (shouldLoadSynchronously) { + wrappedCallback() + } else { + taskIdToUpdatingTask[task.key.id] = wrappedCallback + } + return null + } + + 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 b66b7351bf..d12c0b0a5a 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 @@ -27,12 +27,13 @@ import org.mockito.kotlin.whenever class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { - val taskIdToBitmap: Map = (0..10).associateWith { mock() } + val taskIdToBitmap: MutableMap = + (0..10).associateWith { mock() }.toMutableMap() val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() var shouldLoadSynchronously: Boolean = true /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ - override fun updateThumbnailInBackground( + override fun getThumbnailInBackground( task: Task, callback: Consumer ): CancellableTask? { diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt similarity index 51% rename from tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt index 39ba434dc0..765f0d1dfb 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.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,24 +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.util.TaskVisualsChangeListener -public class FolderPoint { - public Point coord; - public char mType; +class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier { + val listeners = mutableListOf() - public FolderPoint(Point coord, char type) { - this.coord = coord; - mType = type; + override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.add(listener) } - /** - * [A-Z]: Represents a folder and number of icons in the folder is represented by - * the order of letter in the alphabet, A=2, B=3, C=4 ... etc. - */ - public int getNumberIconsInside() { - return (mType - 'A') + 2; + 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 e160627d75..7a17872827 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 @@ -20,21 +20,47 @@ 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()) 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 getThumbnailById(taskId: Int): Flow = + getTaskDataById(taskId).map { it?.thumbnail } override fun setVisibleTasks(visibleTaskIdList: List) { 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 { taskIconData -> + icon = taskIconData?.icon + titleDescription = taskIconData?.contentDescription + title = taskIconData?.title + } + } + } } fun seedTasks(tasks: List) { @@ -44,4 +70,8 @@ class FakeTasksRepository : RecentTasksRepository { fun seedThumbnailData(thumbnailDataMap: Map) { this.thumbnailDataMap = thumbnailDataMap } + + fun seedIconData(iconDataMap: Map) { + this.taskIconDataMap = iconDataMap + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt new file mode 100644 index 0000000000..abe414238d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.FakeInvariantDeviceProfileTest +import com.android.quickstep.views.RecentsViewContainer +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.whenever + +/** Test for [RecentsDeviceProfileRepositoryImpl] */ +@RunWith(AndroidJUnit4::class) +class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() { + private val recentsViewContainer = mock() + + private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer) + + @Test + fun deviceProfileMappedCorrectly() { + initializeVarsForTablet() + val tabletDeviceProfile = newDP() + whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile) + + assertThat(systemUnderTest.getRecentsDeviceProfile()) + .isEqualTo(RecentsDeviceProfile(isLargeScreen = true)) + } +} 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 0000000000..017f037fee --- /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 0000000000..41f6bfdf75 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt @@ -0,0 +1,191 @@ +/* + * 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 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.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions + +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_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() + verify(additionalListener).onHighResLoadingStateChanged() + } + + @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 c28a85a8f8..f31467f06c 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,20 +18,26 @@ 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.drawable.Drawable +import com.android.launcher3.util.TestDispatcherProvider +import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest import com.android.quickstep.util.DesktopTask import com.android.quickstep.util.GroupTask 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.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.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) class TasksRepositoryTest { @@ -44,107 +50,242 @@ class TasksRepositoryTest { ) 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 = + 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) + fun getAllTaskDataReturnsFlattenedListOfTasks() = + testScope.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) + 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 setVisibleTasksPopulatesThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(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(listOf(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(listOf(1, 2)) + + assertThat(systemUnderTest.getTaskDataById(2).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 changingVisibleTasksContainsAlreadyPopulatedIcons() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + + // Prevent new loading of Drawables + taskThumbnailDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(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(listOf(1, 2)) + + val task2 = systemUnderTest.getTaskDataById(2).first()!! + assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2) + task2.assertHasIconDataFromSource(taskIconDataSource) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.shouldLoadSynchronously = false + taskIconDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(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] + taskThumbnailDataSource.shouldLoadSynchronously = false + + // Setup TasksRepository + systemUnderTest.getAllTaskData(forceRefresh = true) + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // Assert there is no bitmap in first emission + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull() + + // Simulate bitmap loading after first emission + taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke() + + // Check for second emission + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(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[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData) + } + + @Test + fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues) + } + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap) + } + + @Test + fun onTaskIconChanged_setsNewIconOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(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[1]).isEqualTo(expectedPreviousIcon) + assertThat(task1IconValues.last()).isEqualTo(expectedIcon) + } 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(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap) + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt new file mode 100644 index 0000000000..02f1d113c8 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt @@ -0,0 +1,137 @@ +/* + * 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.usecase + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +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.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Test for [GetThumbnailPositionUseCase] */ +@RunWith(AndroidJUnit4::class) +class GetThumbnailPositionUseCaseTest { + private val task = + Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.BLACK + } + private val thumbnailData = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + } + ) + + private val deviceProfileRepository = FakeRecentsDeviceProfileRepository() + private val rotationStateRepository = FakeRecentsRotationStateRepository() + private val tasksRepository = FakeTasksRepository() + private val previewPositionHelper = mock() + + private val systemUnderTest = + GetThumbnailPositionUseCase( + deviceProfileRepository, + rotationStateRepository, + tasksRepository, + previewPositionHelper + ) + + @Test + fun invisibleTask_returnsIdentityMatrix() = runTest { + tasksRepository.seedTasks(listOf(task)) + + assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)) + .isInstanceOf(MissingThumbnail::class.java) + } + + @Test + fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest { + tasksRepository.seedTasks(listOf(task)) + tasksRepository.setVisibleTasks(listOf(TASK_ID)) + + assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)) + .isInstanceOf(MissingThumbnail::class.java) + } + + @Test + fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest { + tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData)) + tasksRepository.seedTasks(listOf(task)) + tasksRepository.setVisibleTasks(listOf(TASK_ID)) + + 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) + + assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .isEqualTo(MatrixScaling(MATRIX, isRotated)) + + verify(previewPositionHelper) + .updateThumbnailMatrix( + Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), + thumbnailData, + CANVAS_WIDTH, + CANVAS_HEIGHT, + isLargeScreen, + activityRotation, + isRtl + ) + } + + companion object { + const val TASK_ID = 2 + 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)) + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt new file mode 100644 index 0000000000..12a94cf904 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.usecase + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.task.viewmodel.TaskOverlayViewModelTest +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [GetThumbnailUseCase] */ +class GetThumbnailUseCaseTest { + private val task = + Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.BLACK + } + private val thumbnailData = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + } + ) + + private val tasksRepository = FakeTasksRepository() + private val systemUnderTest = GetThumbnailUseCase(tasksRepository) + + @Test + fun taskNotSeeded_returnsNull() { + assertThat(systemUnderTest.run(TASK_ID)).isNull() + } + + @Test + fun taskNotLoaded_returnsNull() { + tasksRepository.seedTasks(listOf(task)) + + assertThat(systemUnderTest.run(TASK_ID)).isNull() + } + + @Test + fun taskNotVisible_returnsNull() { + tasksRepository.seedTasks(listOf(task)) + tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData)) + + assertThat(systemUnderTest.run(TASK_ID)).isNull() + } + + @Test + fun taskVisible_returnsThumbnail() { + tasksRepository.seedTasks(listOf(task)) + tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData)) + tasksRepository.setVisibleTasks(listOf(TaskOverlayViewModelTest.TASK_ID)) + + assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail) + } + + companion object { + const val TASK_ID = 0 + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt new file mode 100644 index 0000000000..ba4e20692f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.usecase + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +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 org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [SysUiStatusNavFlagsUseCase] */ +class SysUiStatusNavFlagsUseCaseTest { + private lateinit var tasksRepository: FakeTasksRepository + private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase + + @Before + fun setup() { + tasksRepository = FakeTasksRepository() + sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository) + initTaskRepository() + } + + @Test + fun onLightAppearanceReturnExpectedFlags() { + assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID)) + .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME) + } + + @Test + fun onDarkAppearanceReturnExpectedFlags() { + assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID)) + .isEqualTo(FLAGS_APPEARANCE_DARK_THEME) + } + + @Test + fun whenThumbnailIsNullReturnDefault() { + assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID)) + .isEqualTo(FLAGS_DEFAULT) + } + + private fun initTaskRepository() { + val firstTask = + Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.BLACK + } + val firstThumbnailData = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + }, + appearance = APPEARANCE_LIGHT_THEME + ) + + val secondTask = + Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply { + colorBackground = Color.BLACK + } + val secondThumbnailData = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + }, + appearance = APPEARANCE_DARK_THEME + ) + + tasksRepository.seedTasks(listOf(firstTask, secondTask)) + tasksRepository.seedThumbnailData( + mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData) + ) + tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID)) + } + + companion object { + const val FIRST_TASK_ID = 0 + const val SECOND_TASK_ID = 100 + const val UNKNOWN_TASK_ID = 404 + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + const val APPEARANCE_LIGHT_THEME = 24 + const val FLAGS_APPEARANCE_LIGHT_THEME = 5 + const val APPEARANCE_DARK_THEME = 0 + const val FLAGS_APPEARANCE_DARK_THEME = 10 + const val FLAGS_DEFAULT = 0 + } +} 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 0000000000..33d96a8c16 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt @@ -0,0 +1,134 @@ +/* + * 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.quickstep.task.thumbnail.TaskThumbnailViewModelTest +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(listOf(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(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap, rotation = rotation) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt new file mode 100644 index 0000000000..a584d714b2 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt @@ -0,0 +1,152 @@ +/* + * 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.drawable.Drawable +import android.view.Surface +import android.view.Surface.ROTATION_90 +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.data.TaskIconQueryResponse +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.viewmodel.TaskContainerData +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 SplashAlphaUseCaseTest { + private val recentsViewData = RecentsViewData() + private val taskContainerData = TaskContainerData() + private val taskThumbnailViewData = TaskThumbnailViewData() + private val recentTasksRepository = FakeTasksRepository() + private val recentsRotationStateRepository = FakeRecentsRotationStateRepository() + private val systemUnderTest = + SplashAlphaUseCase( + recentsViewData, + taskContainerData, + taskThumbnailViewData, + recentTasksRepository, + recentsRotationStateRepository + ) + + @Test + fun execute_withNullThumbnail_showsSplash() = runTest { + assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_HIDDEN) + } + + @Test + fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest { + setupTask(2) + taskContainerData.thumbnailSplashProgress.value = 0.7f + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f) + } + + @Test + fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest { + setupTask(2) + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN) + } + + @Test + fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() = + runTest { + setupTask(2) + recentsViewData.thumbnailSplashProgress.value = 0.5f + taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2 + taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2 + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN) + } + + @Test + fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest { + setupTask(2) + recentsViewData.thumbnailSplashProgress.value = 0.5f + taskThumbnailViewData.width.value = THUMBNAIL_WIDTH + taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2 + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f) + } + + @Test + fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest { + setupTask(2, createThumbnailData(rotation = ROTATION_90)) + recentsViewData.thumbnailSplashProgress.value = 0.5f + taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2 + taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2 + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f) + } + + @Test + fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest { + setupTask(2, createThumbnailData(rotation = ROTATION_90)) + recentsViewData.thumbnailSplashProgress.value = 0.5f + taskThumbnailViewData.width.value = THUMBNAIL_WIDTH + taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2 + + assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f) + } + + private val tasks = (0..5).map(::createTaskWithId) + + private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) { + recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData)) + val expectedIconData = createIconData("Task $taskId") + recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData)) + recentTasksRepository.seedTasks(tasks) + recentTasksRepository.setVisibleTasks(listOf(taskId)) + } + + 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) + } + + private fun createIconData(title: String) = TaskIconQueryResponse(mock(), "", title) + + private fun createTaskWithId(taskId: Int) = + Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.argb(taskId, taskId, taskId, taskId) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + + const val SPLASH_HIDDEN = 0f + const val SPLASH_SHOWN = 1f + } +} 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 index 3b8754c200..fcf4e564db 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt @@ -20,15 +20,25 @@ import android.content.ComponentName import android.content.Intent import android.graphics.Bitmap import android.graphics.Color -import android.graphics.Rect +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.view.Surface import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.data.TaskIconQueryResponse +import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail 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.SnapshotSplash import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized +import com.android.quickstep.task.viewmodel.TaskContainerData +import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel import com.android.quickstep.task.viewmodel.TaskViewData +import com.android.quickstep.views.TaskViewType import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import com.google.common.truth.Truth.assertThat @@ -39,13 +49,26 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +/** Test for [TaskThumbnailView] */ @RunWith(AndroidJUnit4::class) class TaskThumbnailViewModelTest { + private var taskViewType = TaskViewType.SINGLE private val recentsViewData = RecentsViewData() - private val taskViewData = TaskViewData() + private val taskViewData by lazy { TaskViewData(taskViewType) } + private val taskContainerData = TaskContainerData() private val tasksRepository = FakeTasksRepository() - private val systemUnderTest = - TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository) + private val mGetThumbnailPositionUseCase = mock() + private val splashAlphaUseCase: SplashAlphaUseCase = mock() + private val systemUnderTest by lazy { + TaskThumbnailViewModel( + recentsViewData, + taskViewData, + taskContainerData, + tasksRepository, + mGetThumbnailPositionUseCase, + splashAlphaUseCase, + ) + } private val tasks = (0..5).map(::createTaskWithId) @@ -56,22 +79,62 @@ class TaskThumbnailViewModelTest { @Test fun bindRunningTask_thenStateIs_LiveTile() = runTest { + val taskId = 1 tasksRepository.seedTasks(tasks) - val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true) - systemUnderTest.bind(taskThumbnail) + tasksRepository.setVisibleTasks(listOf(taskId)) + recentsViewData.runningTaskIds.value = setOf(taskId) + systemUnderTest.bind(taskId) assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) } @Test - fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest { + fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() = runTest { + val taskId = 1 + val expectedThumbnailData = createThumbnailData() + tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData)) + val expectedIconData = createIconData("Task 1") + tasksRepository.seedIconData(mapOf(taskId to expectedIconData)) + tasksRepository.seedTasks(tasks) + tasksRepository.setVisibleTasks(listOf(taskId)) + recentsViewData.runningTaskIds.value = setOf(taskId) + recentsViewData.runningTaskShowScreenshot.value = true + systemUnderTest.bind(taskId) + + assertThat(systemUnderTest.uiState.first()) + .isEqualTo( + SnapshotSplash( + Snapshot( + backgroundColor = Color.rgb(1, 1, 1), + bitmap = expectedThumbnailData.thumbnail!!, + thumbnailRotation = Surface.ROTATION_0, + ), + expectedIconData.icon + ) + ) + } + + @Test + fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() = runTest { recentsViewData.fullscreenProgress.value = 0.5f - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f) + assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f) recentsViewData.fullscreenProgress.value = 0.6f - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f) + assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f) + } + + @Test + fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() = runTest { + taskViewType = TaskViewType.DESKTOP + recentsViewData.fullscreenProgress.value = 0.5f + + assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f) + + recentsViewData.fullscreenProgress.value = 0.6f + + assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f) } @Test @@ -85,94 +148,163 @@ class TaskThumbnailViewModelTest { @Test fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() = runTest { + val runningTaskId = 1 + val stoppedTaskId = 2 tasksRepository.seedTasks(tasks) - val runningTask = TaskThumbnail(taskId = 1, isRunning = true) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) - systemUnderTest.bind(runningTask) + tasksRepository.setVisibleTasks(listOf(runningTaskId, stoppedTaskId)) + recentsViewData.runningTaskIds.value = setOf(runningTaskId) + systemUnderTest.bind(runningTaskId) assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) - systemUnderTest.bind(stoppedTask) + systemUnderTest.bind(stoppedTaskId) assertThat(systemUnderTest.uiState.first()) .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) } @Test fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest { + val stoppedTaskId = 2 tasksRepository.seedTasks(tasks) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) + tasksRepository.setVisibleTasks(listOf(stoppedTaskId)) - systemUnderTest.bind(stoppedTask) + systemUnderTest.bind(stoppedTaskId) 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 + val taskId = 2 + tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData())) + tasks[taskId].isLocked = true tasksRepository.seedTasks(tasks) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) + tasksRepository.setVisibleTasks(listOf(taskId)) - systemUnderTest.bind(recentTask) + systemUnderTest.bind(taskId) 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)) + fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() = runTest { + val taskId = 2 + val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270) + tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData)) + val expectedIconData = createIconData("Task 2") + tasksRepository.seedIconData(mapOf(taskId to expectedIconData)) tasksRepository.seedTasks(tasks) - tasksRepository.setVisibleTasks(listOf(2)) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) + tasksRepository.setVisibleTasks(listOf(taskId)) - systemUnderTest.bind(recentTask) + systemUnderTest.bind(taskId) assertThat(systemUnderTest.uiState.first()) .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) + SnapshotSplash( + Snapshot( + backgroundColor = Color.rgb(2, 2, 2), + bitmap = expectedThumbnailData.thumbnail!!, + thumbnailRotation = Surface.ROTATION_270, + ), + expectedIconData.icon ) ) } @Test - fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest { + fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() = runTest { + val taskId = 2 val expectedThumbnailData = createThumbnailData() - tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData)) + tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData)) + val expectedIconData = createIconData("Task 2") + tasksRepository.seedIconData(mapOf(taskId to expectedIconData)) 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)) + systemUnderTest.bind(taskId) + assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) + + tasksRepository.setVisibleTasks(listOf(taskId)) assertThat(systemUnderTest.uiState.first()) .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) + SnapshotSplash( + Snapshot( + backgroundColor = Color.rgb(2, 2, 2), + bitmap = expectedThumbnailData.thumbnail!!, + thumbnailRotation = Surface.ROTATION_0, + ), + expectedIconData.icon ) ) } + @Test + fun getSnapshotMatrix_MissingThumbnail() = runTest { + val taskId = 2 + val isRtl = true + + whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .thenReturn(MissingThumbnail) + + systemUnderTest.bind(taskId) + assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .isEqualTo(Matrix.IDENTITY_MATRIX) + } + + @Test + fun getSnapshotMatrix_MatrixScaling() = runTest { + val taskId = 2 + val isRtl = true + + whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .thenReturn(MatrixScaling(MATRIX, isRotated = false)) + + systemUnderTest.bind(taskId) + assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .isEqualTo(MATRIX) + } + + @Test + fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() = runTest { + recentsViewData.tintAmount.value = 0.32f + taskContainerData.taskMenuOpenProgress.value = 0f + assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f) + } + + @Test + fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() = runTest { + recentsViewData.tintAmount.value = 0f + taskContainerData.taskMenuOpenProgress.value = 1f + assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f) + } + + @Test + fun getForegroundScrimDimProgress_returnsNoScrim() = runTest { + recentsViewData.tintAmount.value = 0f + taskContainerData.taskMenuOpenProgress.value = 0f + assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f) + } + 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 { + 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) + return ThumbnailData(thumbnail = bitmap, rotation = rotation) } + private fun createIconData(title: String) = TaskIconQueryResponse(mock(), "", title) + 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)) + } } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt new file mode 100644 index 0000000000..d0887df9bf --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt @@ -0,0 +1,191 @@ +/* + * 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.viewmodel + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Matrix +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling +import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled +import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled +import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState +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 + +/** Test for [TaskOverlayViewModel] */ +@RunWith(AndroidJUnit4::class) +class TaskOverlayViewModelTest { + private val task = + Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.BLACK + } + private val thumbnailData = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + } + ) + private val recentsViewData = RecentsViewData() + private val tasksRepository = FakeTasksRepository() + private val mGetThumbnailPositionUseCase = mock() + private val systemUnderTest = + TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository) + + @Test + fun initialStateIsDisabled() = runTest { + assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) + } + + @Test + fun recentsViewOverlayDisabled_Disabled() = runTest { + recentsViewData.overlayEnabled.value = false + recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID) + + assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) + } + + @Test + fun taskNotFullyVisible_Disabled() = runTest { + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf() + + assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) + } + + @Test + fun noThumbnail_Enabled() = runTest { + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID) + task.isLocked = false + + assertThat(systemUnderTest.overlayState.first()) + .isEqualTo( + Enabled( + isRealSnapshot = false, + thumbnail = null, + ) + ) + } + + @Test + fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest { + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID) + tasksRepository.seedTasks(listOf(task)) + tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData)) + tasksRepository.setVisibleTasks(listOf(TASK_ID)) + thumbnailData.isRealSnapshot = true + task.isLocked = false + + assertThat(systemUnderTest.overlayState.first()) + .isEqualTo( + Enabled( + isRealSnapshot = true, + thumbnail = thumbnailData.thumbnail, + ) + ) + } + + @Test + fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest { + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID) + tasksRepository.seedTasks(listOf(task)) + tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData)) + tasksRepository.setVisibleTasks(listOf(TASK_ID)) + thumbnailData.isRealSnapshot = true + task.isLocked = true + + assertThat(systemUnderTest.overlayState.first()) + .isEqualTo( + Enabled( + isRealSnapshot = false, + thumbnail = thumbnailData.thumbnail, + ) + ) + } + + @Test + fun withThumbnail_FakeSnapshot_Enabled() = runTest { + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID) + tasksRepository.seedTasks(listOf(task)) + tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData)) + tasksRepository.setVisibleTasks(listOf(TASK_ID)) + thumbnailData.isRealSnapshot = false + task.isLocked = false + + assertThat(systemUnderTest.overlayState.first()) + .isEqualTo( + Enabled( + isRealSnapshot = false, + thumbnail = thumbnailData.thumbnail, + ) + ) + } + + @Test + fun getThumbnailMatrix_MissingThumbnail() = runTest { + val isRtl = true + + whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .thenReturn(MissingThumbnail) + + assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false)) + } + + @Test + fun getThumbnailMatrix_MatrixScaling() = runTest { + val isRtl = true + val isRotated = true + + whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .thenReturn(MatrixScaling(MATRIX, isRotated)) + + assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)) + .isEqualTo(ThumbnailPositionState(MATRIX, isRotated)) + } + + companion object { + const val TASK_ID = 0 + 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)) + } + } +} 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 b637e7d4cf..d66197a6fb 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/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt index ece67aff62..99d31218cb 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt @@ -28,9 +28,9 @@ 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.split.SplitScreenConstants.SNAP_TO_30_70 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Before 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 0000000000..7aed579f44 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.content.ComponentName +import android.content.Intent +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(createTasks(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testDesktopTask_identicalConstructor_isEqual() { + val task1 = DesktopTask(createTasks(1)) + val task2 = DesktopTask(createTasks(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_copy_isEqual() { + val task1 = DesktopTask(createTasks(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_differentId_isNotEqual() { + val task1 = DesktopTask(createTasks(1)) + val task2 = DesktopTask(createTasks(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_differentLength_isNotEqual() { + val task1 = DesktopTask(createTasks(1)) + val task2 = DesktopTask(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/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt new file mode 100644 index 0000000000..7b1c066970 --- /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 com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.views.TaskViewType +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 = GroupTask(createTask(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testGroupTask_identicalConstructor_isEqual() { + val task1 = GroupTask(createTask(1)) + val task2 = GroupTask(createTask(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_copy_isEqual() { + val task1 = GroupTask(createTask(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentId_isNotEqual() { + val task1 = GroupTask(createTask(1)) + val task2 = GroupTask(createTask(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_equalSplitTasks_isEqual() { + val splitBounds = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_50_50 + ) + val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED) + val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentSplitTasks_isNotEqual() { + val splitBounds1 = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_50_50 + ) + val splitBounds2 = + SplitConfigurationOptions.SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_30_70 + ) + val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED) + val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_differentType_isNotEqual() { + val task1 = GroupTask(createTask(1), null, null, TaskViewType.SINGLE) + val task2 = GroupTask(createTask(1), null, null, TaskViewType.DESKTOP) + 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/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index d40f8ab389..505125132f 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,16 @@ class SplitAnimationControllerTest { @Before fun setup() { - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) - whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView) + whenever(mockTaskContainer.splitAnimationThumbnail).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(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable) whenever(splitSelectSource.view).thenReturn(mockSplitSourceView) + whenever(splitSelectSource.itemInfo).thenReturn(mockItemInfo) splitAnimationController = SplitAnimationController(mockSplitSelectStateController) } @@ -180,7 +183,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) @@ -227,7 +229,8 @@ class SplitAnimationControllerTest { depthController, null /* info */, null /* t */, - {} /* finishCallback */ + {} /* finishCallback */, + 1f /* cornerRadius */ ) verify(spySplitAnimationController) @@ -263,7 +266,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f /* cornerRadius */ ) verify(spySplitAnimationController) @@ -276,7 +280,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 +295,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 +324,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f /* cornerRadius */ ) verify(spySplitAnimationController) @@ -346,7 +352,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f /* cornerRadius */ ) verify(spySplitAnimationController) @@ -373,7 +380,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f /* cornerRadius */ ) verify(spySplitAnimationController) @@ -385,7 +393,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 +407,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 bab84ef28b..fc4c4f60a7 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -39,7 +39,7 @@ import com.android.quickstep.SystemUiProxy import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController 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_50_50 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse 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 7ef4910ce5..0000000000 --- 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 0000000000..7aab75f590 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt @@ -0,0 +1,638 @@ +/* + * 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.CLEAR_ALL_PLACEHOLDER_ID +import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN +import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT +import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT +import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB +import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_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, DIRECTION_DOWN, delta = 1)) + .isEqualTo(2) + } + + /* ↑----→ + 5 3 1 | + CLEAR_ALL | + 6 4 2←---| + */ + @Test + fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2) + } + + /* ↓----↑ + 5 3 1 | + CLEAR_ALL | + 6 4 2 | + ↓----→ + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1)) + .isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1) + } + + /* + 5 3<--1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1)) + .isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL + 6 4<--2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_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, DIRECTION_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, DIRECTION_RIGHT, delta = -1)) + .isEqualTo(2) + } + + /* + ↓------------------← + | | + ↓ 5 3 1---→ + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_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, DIRECTION_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, DIRECTION_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, DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, DIRECTION_TAB, delta = 1)) + .isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + ←---↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1)) + .isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL ↓ + ----→ + ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1)) + .isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1)) + .isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK←--DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressLeftFromDesktopTask_goesToFocusedTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_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, + DIRECTION_TAB, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + private fun getNextGridPage( + currentPageTaskViewId: Int, + direction: Int, + delta: Int, + topIds: IntArray = IntArray.wrap(1, 3, 5), + bottomIds: IntArray = IntArray.wrap(2, 4, 6), + largeTileIds: List = emptyList(), + ): Int { + val taskGridNavHelper = TaskGridNavHelper(topIds, bottomIds, largeTileIds) + return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true) + } + + 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/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt new file mode 100644 index 0000000000..6c526a4855 --- /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/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt index 15b1e532bd..d064f4aef7 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -57,6 +57,7 @@ abstract class TaskbarBaseTestCase { @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController @Mock lateinit var taskbarPinningController: TaskbarPinningController @Mock lateinit var optionalBubbleControllers: Optional + @Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController lateinit var taskbarControllers: TaskbarControllers @@ -98,6 +99,7 @@ abstract class TaskbarBaseTestCase { keyboardQuickSwitchController, taskbarPinningController, optionalBubbleControllers, + taskbarDesktopModeController ) } } diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java index 9ed39066dd..67a0ee4270 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java @@ -39,6 +39,7 @@ import android.view.MotionEvent; import androidx.test.filters.SmallTest; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.model.data.FolderInfo; @@ -66,6 +67,7 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { @Mock private MotionEvent mMotionEvent; @Mock private BubbleTextView mHoverBubbleTextView; @Mock private FolderIcon mHoverFolderIcon; + @Mock private AppPairIcon mAppPairIcon; @Mock private Display mDisplay; @Mock private TaskbarDragLayer mTaskbarDragLayer; private Folder mSpyFolderView; @@ -85,6 +87,7 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer); when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper()); when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay); + when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false); when(mTaskbarDragLayer.getChildCount()).thenReturn(1); mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null)); @@ -199,7 +202,7 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { boolean hoverHandled = mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - assertThat(hoverHandled).isFalse(); + assertThat(hoverHandled).isTrue(); } @Test @@ -210,7 +213,50 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { boolean hoverHandled = mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - assertThat(hoverHandled).isFalse(); + assertThat(hoverHandled).isTrue(); + } + + @Test + public void onHover_hoverEnterAppPair_revealToolTip() { + when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + + boolean hoverHandled = + mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent); + waitForIdleSync(); + + assertThat(hoverHandled).isTrue(); + verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, + true); + } + + @Test + public void onHover_hoverExitAppPair_closeToolTip() { + when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); + when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); + + boolean hoverHandled = + mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent); + waitForIdleSync(); + + assertThat(hoverHandled).isTrue(); + verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, + false); + } + + @Test + public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() { + when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true); + + boolean hoverHandled = + mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); + waitForIdleSync(); + + assertThat(hoverHandled).isTrue(); + verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, + true); } private void waitForIdleSync() { diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt index 104263af5b..b67bc5a952 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -16,227 +16,823 @@ package com.android.launcher3.taskbar -import android.app.ActivityManager.RunningTaskInfo 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.os.Process import android.os.UserHandle +import android.platform.test.rule.TestWatcher import android.testing.AndroidTestingRunner +import com.android.internal.R +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.statehandlers.DesktopVisibilityController +import com.android.launcher3.model.data.TaskItemInfo 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.systemui.shared.recents.model.Task 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.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 @RunWith(AndroidTestingRunner::class) 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 mockDesktopVisibilityController: DesktopVisibilityController + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources - private var nextTaskId: Int = 500 + private var taskListChangeId: Int = 1 private lateinit var recentAppsController: TaskbarRecentAppsController private lateinit var userHandle: UserHandle + private var canShowRunningAndRecentAppsAtInit = true + private var recentTasksChangedListener: RecentTasksChangedListener? = null + @Before fun setUp() { super.setup() userHandle = Process.myUserHandle() - recentAppsController = - TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController } + + // 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) - recentAppsController.isEnabled = true - recentAppsController.setApps( - ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray() + 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 updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) + fun canShowRunningAndRecentAppsIsFalseAfterInit_getTasksOnlyCalledInInit() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(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>>()) } @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() - + fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) 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() { + fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRecentApps = false 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 }, - ) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) + } - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) - assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3) + @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) + } + + @Test + fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_hotseatCoversFirstTask() { + 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(), + ) + + // First 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.taskId).isEqualTo(1) + // Second task is in shownTasks + val shownTasks = recentAppsController.shownTasks.map { it.task1 } + assertThat(shownTasks) + .containsExactlyElementsIn(listOf(createTask(id = 2, HOTSEAT_PACKAGE_1))) + } + + @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(), + ) + val shownTasks = recentAppsController.shownTasks.map { it.task1 } + assertThat(shownTasks).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(), + ) + + val shownTasks = recentAppsController.shownTasks.map { it.task1 } + assertThat(shownTasks).isEqualTo(listOf(task1, task2)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_multiInstance_shownTasks_maintainsOrder() { + 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(), + ) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + + val shownTasks = recentAppsController.shownTasks.map { it.task1 } + assertThat(shownTasks).isEqualTo(listOf(task1, task2)) + } + + @Test + fun updateHotseatItems_inDesktopMode_multiInstanceHotseatPackage_shownItems_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(RUNNING_APP_PACKAGE_1), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + updateRecentTasks( // Trigger a recent-tasks change before calling updateHotseatItems() + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(RUNNING_APP_PACKAGE_1), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + + val newHotseatItems = recentAppsController.shownHotseatItems + assertThat(newHotseatItems).hasSize(1) + assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java) + assertThat((newHotseatItems[0] as TaskItemInfo).taskId).isEqualTo(1) + val shownTasks = recentAppsController.shownTasks.map { it.task1 } + assertThat(shownTasks).isEqualTo(listOf(task2)) + } + + @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() + } + + private fun prepareHotseatAndRunningAndRecentApps( + hotseatPackages: List, + runningTasks: List, + recentTaskPackages: List, + ): Array { + val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages) + 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(ArrayList(runningTasks))) + } + addAll(recentTasks) + } + doAnswer { + val callback: Consumer> = it.getArgument(0) + callback.accept(allTasks) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any>>()) + recentTasksChangedListener?.onRecentTasksChanged() } 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() - } + return packageNames + .map { + createTestAppInfo(packageName = it).apply { + container = + if (it.startsWith("predicted")) { + CONTAINER_HOTSEAT_PREDICTION + } else { + CONTAINER_HOTSEAT + } + } + } + .map { it.makeWorkspaceItem(taskbarActivityContext) } } private fun createTestAppInfo( packageName: String = "testPackageName", - className: String = "testClassName" + className: String = "testClassName", ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) - private fun setInDesktopMode(inDesktopMode: Boolean) { - whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode) + private fun createRecentTasksFromPackageNames(packageNames: List): List { + return packageNames.map { packageName -> + if (packageName.startsWith("split")) { + val splitPackages = packageName.split("_") + GroupTask( + createTask(100, splitPackages[0]), + createTask(101, splitPackages[1]), + /* splitBounds = */ null, + ) + } else { + // Use the number at the end of the test packageName as the id. + val id = 1000 + packageName[packageName.length - 1].code + GroupTask(createTask(id, packageName)) + } + } } + private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task { + return Task( + Task.TaskKey( + id, + WINDOWING_MODE_FREEFORM, + Intent().apply { `package` = packageName }, + ComponentName(packageName, "TestActivity"), + userHandle.identifier, + 0, + ) + ) + .apply { this.isVisible = isVisible } + } + + private fun setInDesktopMode(inDesktopMode: Boolean) { + whenever(taskbarControllers.taskbarDesktopModeController.areDesktopTasksVisible) + .thenReturn(inDesktopMode) + } + + 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" - val ALL_APP_PACKAGES = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - RUNNING_APP_PACKAGE_3, - ) + 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" } } diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt index 50b5df13f2..885a7f6b7a 100644 --- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt @@ -32,14 +32,15 @@ import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.quickstep.TaskOverlayFactory.TaskOverlay import com.android.quickstep.views.LauncherRecentsView +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.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 @@ -186,10 +187,10 @@ class DesktopSystemShortcutTest { } } - private fun createTaskContainer(task: Task): TaskView.TaskContainer { - return taskView.TaskContainer( + private fun createTaskContainer(task: Task): TaskContainer { + return TaskContainer( + taskView, task, - thumbnailView = null, thumbnailViewDeprecated, iconView, transformingTouchDelegate, diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java index 298dd6cae0..f5d082dd5c 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; @@ -288,6 +289,34 @@ public class OrientationTouchTransformerTest { assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY())); } + @Test + public void testSimpleOrientationTouchTransformer() { + final DisplayController displayController = mock(DisplayController.class); + doReturn(mInfo).when(displayController).getInfo(); + final SimpleOrientationTouchTransformer transformer = + new SimpleOrientationTouchTransformer(getApplicationContext(), displayController); + 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 index 03244eb0bf..244b897de7 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java +++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java @@ -16,6 +16,8 @@ package com.android.quickstep; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.TestCase.assertNull; import static org.junit.Assert.assertEquals; @@ -27,12 +29,17 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.Resources; import androidx.test.filters.SmallTest; +import com.android.internal.R; import com.android.launcher3.util.LooperExecutor; import com.android.quickstep.util.GroupTask; -import com.android.wm.shell.util.GroupedRecentTaskInfo; +import com.android.quickstep.views.TaskViewType; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import org.junit.Before; import org.junit.Test; @@ -40,14 +47,21 @@ 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 public class RecentTasksListTest { @Mock - private SystemUiProxy mockSystemUiProxy; + private Context mContext; + @Mock + private Resources mResources; + @Mock + private SystemUiProxy mSystemUiProxy; @Mock private TopTaskTracker mTopTaskTracker; @@ -59,22 +73,27 @@ public class RecentTasksListTest { MockitoAnnotations.initMocks(this); LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class); KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); - mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager, - mockSystemUiProxy, mTopTaskTracker); + + // Set desktop mode supported + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true); + + mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor, + mockKeyguardManager, mSystemUiProxy, mTopTaskTracker); } @Test - public void onRecentTasksChanged_doesNotFetchTasks() { + public void onRecentTasksChanged_doesNotFetchTasks() throws Exception { mRecentTasksList.onRecentTasksChanged(); - verify(mockSystemUiProxy, times(0)) + verify(mSystemUiProxy, times(0)) .getRecentTasks(anyInt(), anyInt()); } @Test - public void loadTasksInBackground_onlyKeys_noValidTaskDescription() { + public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception { GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks( new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, @@ -86,7 +105,19 @@ public class RecentTasksListTest { } @Test - public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() { + 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 + public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception { String taskDescription = "Wheeee!"; ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo(); task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); @@ -94,7 +125,7 @@ public class RecentTasksListTest { task2.taskDescription = new ActivityManager.TaskDescription(); GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2, null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, @@ -104,4 +135,53 @@ public class RecentTasksListTest { assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel()); assertNull(taskList.get(0).task2.taskDescription.getLabel()); } + + @Test + public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception { + ActivityManager.RecentTaskInfo[] tasks = { + createRecentTaskInfo(1 /* taskId */), + createRecentTaskInfo(4 /* taskId */), + createRecentTaskInfo(5 /* taskId */)}; + GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks( + 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(3, actualFreeformTasks.size()); + assertEquals(1, actualFreeformTasks.get(0).key.id); + assertEquals(4, actualFreeformTasks.get(1).key.id); + assertEquals(5, actualFreeformTasks.get(2).key.id); + } + + @Test + public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() + throws Exception { + ActivityManager.RecentTaskInfo[] tasks = { + createRecentTaskInfo(1 /* taskId */), + createRecentTaskInfo(4 /* taskId */), + createRecentTaskInfo(5 /* taskId */)}; + Set minimizedTaskIds = + Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet()); + GroupedRecentTaskInfo recentTaskInfos = + GroupedRecentTaskInfo.forFreeformTasks(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(0, taskList.size()); + } + + private ActivityManager.RecentTaskInfo createRecentTaskInfo(int taskId) { + ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo(); + recentTaskInfo.taskId = taskId; + return recentTaskInfo; + } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java index 07d8f61992..e981570635 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java +++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java @@ -31,6 +31,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.Launcher; 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; @@ -66,15 +67,15 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { mLauncher.goHome(); 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()); + assertFalse("Toast is visible", getToast().getHasLimit()); } finally { runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); @@ -86,7 +87,7 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher)); return getFromLauncher(launcher -> { - TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0); + TaskContainer taskContainer = task.getTaskContainers().get(0); assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals( taskContainer.getTask().getTopComponent().getPackageName())); return taskContainer.getDigitalWellBeingToast(); diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java index b7fd8be311..9bc1c5981e 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.Overview; import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask; 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 1886ce671a..a8f39afd5a 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java @@ -31,6 +31,10 @@ public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest { static final int STRESS_REPEAT_COUNT = 10; + private enum TestCase { + TO_HOME, TO_OVERVIEW, + } + @Override @Before public void setUp() throws Exception { @@ -41,28 +45,55 @@ 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) { for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { // 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); + } + } + 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/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt new file mode 100644 index 0000000000..694a3822fe --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt @@ -0,0 +1,105 @@ +/* + * 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.ui.AbstractLauncherUiTest +import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape +import com.android.launcher3.uioverrides.QuickstepLauncher +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() { + // Move last launched TEST_ACTIVITY_2 into Desktop + mLauncher.workspace + .switchToOverview() + .getTestActivityTask(TEST_ACTIVITY_2) + .tapMenu() + .tapDesktopMenuItem() + assertTestAppLaunched(TEST_ACTIVITY_2) + + // Scroll back to TEST_ACTIVITY_1, then move it into Desktop + mLauncher + .goHome() + .switchToOverview() + .apply { flingForward() } + .getTestActivityTask(TEST_ACTIVITY_1) + .tapMenu() + .tapDesktopMenuItem() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch static DesktopTaskView + val desktop = + mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch live-tile DesktopTaskView + desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + } + + 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")), + DEFAULT_UI_TIMEOUT + ) + ) + .isTrue() + } + + companion object { + const val TEST_ACTIVITY_1 = 2 + const val TEST_ACTIVITY_2 = 3 + 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 df73e0913e..a16811efaf 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 7877e8ac22..113b8a437f 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -266,9 +266,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { return launcher.getOverviewPanel().getBottomRowTaskCountForTablet(); } - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -293,9 +290,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -401,6 +395,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @Test @NavigationModeSwitch @PortraitLandscape + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406 public void testQuickSwitchFromHome() throws Exception { startTestActivity(2); mLauncher.goHome().quickSwitchToPreviousApp(); @@ -482,7 +477,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { // 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", +// executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple +// dismissals", // (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet( // launcher)) <= 1))); @@ -581,7 +577,7 @@ 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 + // TODO(b/342627272): 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") @@ -591,7 +587,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { if (overview.hasTasks()) { currentTask = overview.getCurrentTask(); assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home", - currentTask.containsContentDescription("ExcludeFromRecents") + currentTask.containsContentDescription( + "ExcludeFromRecents") || currentTask.containsContentDescription(null)); } else { // Presumably the test started with 0 tasks and remains that way after going home. diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java index 8adf79318b..daa4ec327a 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java @@ -17,8 +17,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; @@ -33,7 +31,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.quickstep.util.SplitScreenTestUtils; import com.android.wm.shell.Flags; import org.junit.After; @@ -72,7 +70,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()); @@ -111,9 +108,8 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { assumeTrue("App pairs feature is currently not enabled, no test needed", Flags.enableAppPairs()); - createAndLaunchASplitPair(); + Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher); - Overview overview = mLauncher.goHome().switchToOverview(); if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) { assertTrue("Save app pair menu item is missing", overview.getCurrentTask() @@ -157,24 +153,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/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java index ec245ee0f3..c24e9742ce 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java @@ -24,6 +24,7 @@ import static com.android.quickstep.TaplTestsTaskbar.TaskbarMode.TRANSIENT; import androidx.test.filters.LargeTest; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.rule.ScreenRecordRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -133,6 +134,7 @@ public class TaplTestsTaskbar extends AbstractTaplTestsTaskbar { @Test @PortraitLandscape + @ScreenRecordRule.ScreenRecord // b/349439239 public void testLaunchAppInSplitscreen_fromTaskbarAllApps() { getTaskbar().openAllApps() .getAppIcon(TEST_APP_NAME) diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java index 2c23f867cb..710ad6f6dc 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/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java new file mode 100644 index 0000000000..28c8a4ac18 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class TaskAnimationManagerTest { + + @Mock + private Context mContext; + + @Mock + private SystemUiProxy mSystemUiProxy; + + private TaskAnimationManager mTaskAnimationManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskAnimationManager = new TaskAnimationManager(mContext) { + @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(); + mTaskAnimationManager.startRecentsAnimation(gestureState, new Intent(), listener); + + final ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(ActivityOptions.class); + verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any()); + assertEquals(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS, + optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode()); + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java index 512557bf3a..dc1da69342 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/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt index 4d10f0f51e..cb59f7dfda 100644 --- a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt +++ b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt @@ -55,7 +55,6 @@ 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 @@ -71,8 +70,9 @@ 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.areDesktopTasksVisible) + .thenAnswer { _ -> isInDesktopMode } + pinningController = spy(TaskbarPinningController(taskbarActivityContext)) pinningController.init(taskbarControllers, taskbarSharedState) } 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 0000000000..82361aabb8 --- /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.OverviewSplitTask.SPLIT_TOP_OR_LEFT + ) + currentTask.containsContentDescription( + By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(), + OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT + ) + return overviewWithSplitPair + } + + private fun clearAllRecentTasks(launcher: LauncherInstrumentation) { + if (launcher.recentTasks.isNotEmpty()) { + launcher.goHome().switchToOverview().dismissAllTasks() + } + } +} 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 3d7ad2bd60..0000000000 --- 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 fb63602d4e..0000000000 --- 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 cba74ba0ec..0000000000 --- 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 22e878d7f1..0000000000 --- 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 e54f95380e..0000000000 --- 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 40f0d4c9de..0000000000 --- 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 24f559b494..4396f6d013 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_dim.xml b/res/color-night-v31/material_color_surface_dim.xml deleted file mode 100644 index a645f24dd9..0000000000 --- a/res/color-night-v31/material_color_surface_dim.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ 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 ac63072a9e..0000000000 --- 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/material_color_surface_variant.xml b/res/color-night-v31/material_color_surface_variant.xml deleted file mode 100644 index a645f24dd9..0000000000 --- a/res/color-night-v31/material_color_surface_variant.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/popup_color_background.xml b/res/color-night-v31/popup_color_background.xml deleted file mode 100644 index 13ceaa09e7..0000000000 --- a/res/color-night-v31/popup_color_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml index 83822a62bc..28995e32bd 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.xml b/res/color-v31/material_color_surface.xml deleted file mode 100644 index b049851ff4..0000000000 --- a/res/color-v31/material_color_surface.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file 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 b049851ff4..0000000000 --- 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 b031c081a9..0000000000 --- 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 a996d51eba..0000000000 --- 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 e7a535af53..0000000000 --- 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 b8fe01e484..0000000000 --- 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 25e8666862..f726aea081 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 e2d226fa89..0000000000 --- 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 e2d226fa89..0000000000 --- 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_color_background.xml b/res/color-v31/popup_color_background.xml deleted file mode 100644 index 99155d8972..0000000000 --- a/res/color-v31/popup_color_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml index 1278bb494a..be73698c2e 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 1dd8da60c7..0b317bd395 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 1278bb494a..be73698c2e 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 47f2a5d73a..0000000000 --- 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 e279fa051a..39af989a1d 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 1e7cff2e93..62927afeb7 100644 --- a/res/drawable/all_apps_tabs_background.xml +++ b/res/drawable/all_apps_tabs_background.xml @@ -30,7 +30,7 @@ android:state_selected="false"> - + @@ -39,7 +39,7 @@ android:state_selected="true"> - + diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/drawable/bg_letter_list_text.xml similarity index 57% rename from res/color-night-v31/material_color_surface_container.xml rename to res/drawable/bg_letter_list_text.xml index 002b88eba4..427702b045 100644 --- a/res/color-night-v31/material_color_surface_container.xml +++ b/res/drawable/bg_letter_list_text.xml @@ -1,6 +1,5 @@ - - - - - \ No newline at end of file + + + + + \ 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 379e0e53d6..a19465dba1 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/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml index f5b68866cb..13959f6925 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 0000000000..6f7c95aac2 --- /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 0000000000..ef15f9f735 --- /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 0000000000..b96a596e94 --- /dev/null +++ b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/color-night-v31/material_color_surface.xml b/res/drawable/ic_bubble_button.xml similarity index 53% rename from res/color-night-v31/material_color_surface.xml rename to res/drawable/ic_bubble_button.xml index a645f24dd9..1ed212e09a 100644 --- a/res/color-night-v31/material_color_surface.xml +++ b/res/drawable/ic_bubble_button.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file + + + diff --git a/res/color-v31/material_color_surface_inverse.xml b/res/drawable/ic_close_work_edu.xml similarity index 53% rename from res/color-v31/material_color_surface_inverse.xml rename to res/drawable/ic_close_work_edu.xml index e189862856..e4053e3ba7 100644 --- a/res/color-v31/material_color_surface_inverse.xml +++ b/res/drawable/ic_close_work_edu.xml @@ -1,6 +1,5 @@ - - - - - \ 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 0000000000..f54285c444 --- /dev/null +++ b/res/drawable/ic_desktop_with_bg.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/res/drawable/ic_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml new file mode 100644 index 0000000000..c4659f821f --- /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_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml index cb37c9add5..cc73f6dafe 100644 --- a/res/drawable/ic_private_space_with_background.xml +++ b/res/drawable/ic_private_space_with_background.xml @@ -13,14 +13,13 @@ limitations under the License. --> + android:fillColor="?attr/materialColorSurfaceContainerLowest" /> - - - - - - - - - - - - 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 6e740aed4f..0000000000 --- a/res/drawable/ic_transient_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml index 2eb1dfcde5..6345c2b007 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="?attr/materialColorSurfaceBright" + android:endColor="?attr/materialColorSurfaceBright" /> \ No newline at end of file diff --git a/res/drawable/popup_background.xml b/res/drawable/popup_background.xml index 6eedecb6ca..4ddd228c68 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/rounded_action_button.xml b/res/drawable/rounded_action_button.xml index 81e94f7e2b..ebfa996ac5 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="?attr/materialColorSurfaceContainerLow" /> diff --git a/res/drawable/widget_picker_tabs_background.xml b/res/drawable/widget_picker_tabs_background.xml index a874dd8b90..f6607b7ad1 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/work_card.xml b/res/drawable/work_card.xml index 4a66cacd72..01ec947158 100644 --- a/res/drawable/work_card.xml +++ b/res/drawable/work_card.xml @@ -16,9 +16,8 @@ - + diff --git a/res/layout/all_apps_fast_scroller.xml b/res/layout/all_apps_fast_scroller.xml index 0f1d9330a6..7e16ca5c0b 100644 --- a/res/layout/all_apps_fast_scroller.xml +++ b/res/layout/all_apps_fast_scroller.xml @@ -36,4 +36,17 @@ android:layout_marginEnd="@dimen/fastscroll_end_margin" launcher:canThumbDetach="true" /> + \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/layout/bubble_bar_overflow_button.xml similarity index 64% rename from res/color-night-v31/material_color_surface_bright.xml rename to res/layout/bubble_bar_overflow_button.xml index f34ed6c548..cb54990c86 100644 --- a/res/color-night-v31/material_color_surface_bright.xml +++ b/res/layout/bubble_bar_overflow_button.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/layout/fast_scroller_letter_list_text_view.xml similarity index 51% rename from res/color-night-v31/material_color_surface_container_high.xml rename to res/layout/fast_scroller_letter_list_text_view.xml index edd36fcd23..493b6fcb7e 100644 --- a/res/color-night-v31/material_color_surface_container_high.xml +++ b/res/layout/fast_scroller_letter_list_text_view.xml @@ -1,6 +1,5 @@ - - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml index a709fbc7a4..83c8d6cba5 100644 --- a/res/layout/launcher.xml +++ b/res/layout/launcher.xml @@ -51,7 +51,7 @@ - @@ -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/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml index 8dc785af66..622f0d6aa5 100644 --- a/res/layout/widgets_full_sheet_paged_view.xml +++ b/res/layout/widgets_full_sheet_paged_view.xml @@ -104,7 +104,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 +116,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_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml index bb2b7bd744..ce5eed93ee 100644 --- a/res/layout/widgets_two_pane_sheet.xml +++ b/res/layout/widgets_two_pane_sheet.xml @@ -48,20 +48,23 @@ android:textSize="24sp" /> + android:lineHeight="20sp" + android:textSize="14sp" /> + android:layout_below="@id/widget_picker_description"> + + - + - - + + + + + - + - - + + + + + + android:background="@drawable/rounded_action_button"> + android:src="@drawable/ic_close_work_edu" /> diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml index 1a8f8e26af..490a7c249f 100644 --- a/res/values-af/strings.xml +++ b/res/values-af/strings.xml @@ -38,6 +38,8 @@ "Apppaar is nie beskikbaar nie" "Raak en hou om \'n legstuk te skuif." "Dubbeltik en hou om \'n legstuk te skuif of gebruik gepasmaakte handelinge." + "Meer opsies" + "Wys alle legstukke" "%1$d × %2$d" "%1$d breed by %2$d hoog" "%1$s-legstuk" @@ -90,6 +92,7 @@ "Installeer" "Moenie voorstel nie" "Vasspeldvoorspelling" + "Borrel" "installeer kortpaaie" "Laat \'n program toe om kortpaaie by te voeg sonder gebruikerinmenging." "lees tuis-instellings en -kortpaaie" diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index 80447c5940..7292eec635 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -38,6 +38,8 @@ "የመተግበሪያ ጥምረት አይገኝም" "ምግብርን ለማንቀሳቀስ ይንኩ እና ይያዙ።" "ምግብርን ለማንቀሳቀስ ወይም ብጁ እርምጃዎችን ለመጠቀም ሁለቴ መታ ያድርጉ እና ይያዙ።" + "ተጨማሪ አማራጮች" + "ሁሉንም ምግብሮች አሳይ" "%1$d × %2$d" "%1$d ስፋት በ%2$d ከፍታ" "የ%1$s ምግብር" @@ -90,6 +92,7 @@ "ጫን" "መተግበሪያውን አይጠቁሙ" "የፒን ግምት" + "አረፋ" "አቋራጮችን ይጭናል" "መተግበሪያው ያለተጠቃሚ ጣልቃ ገብነት አቋራጭ እንዲያክል ያስችለዋል።" "የመነሻ ቅንብሮች እና አቋራጮችን ያነባል" diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 4eee1217a7..06fc0a8dd1 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -38,6 +38,8 @@ "ميزة \"استخدام تطبيقين في الوقت نفسه\" غير متوفّرة" "انقر مع الاستمرار لنقل أداة." "انقر مرتين مع تثبيت إصبعك لنقل أداة أو استخدام الإجراءات المخصّصة." + "خيارات إضافية" + "عرض كل التطبيقات المصغّرة" "%1$d × %2$d" "‏العرض %1$d الطول %2$d" "أداة %1$s" @@ -90,6 +92,7 @@ "تثبيت" "عدم اقتراح التطبيق" "تثبيت التطبيق المتوقّع" + "فقاعة" "تثبيت اختصارات" "للسماح لتطبيق ما بإضافة اختصارات بدون تدخل المستخدم." "الاطلاع على الإعدادات والاختصارات على الشاشة الرئيسية" diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml index 52ec7ea8dd..cd6e347684 100644 --- a/res/values-as/strings.xml +++ b/res/values-as/strings.xml @@ -38,6 +38,8 @@ "এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো উপলব্ধ নহয়" "ৱিজেট স্থানান্তৰ কৰিবলৈ টিপি ধৰি ৰাখক।" "কোনো ৱিজেট স্থানান্তৰ কৰিবলৈ দুবাৰ টিপি ধৰি ৰাখক অথবা কাষ্টম কাৰ্য ব্যৱহাৰ কৰক।" + "অধিক বিকল্প" + "আটাইবোৰ ৱিজেট দেখুৱাওক" "%1$d × %2$d" "%1$d বহল x %2$d ওখ" "%1$s ৱিজেট" @@ -90,6 +92,7 @@ "ইনষ্টল কৰক" "পৰামৰ্শ নিদিব" "পূৰ্বানুমান কৰা এপ্‌টো পিন কৰক" + "বাবল" "শ্বৰ্টকাট ইনষ্টল কৰিব পাৰে" "ব্য়ৱহাৰকাৰীৰ হস্তক্ষেপ অবিহনেই কোনো এপক শ্বৰ্টকাটবোৰ যোগ কৰাৰ অনুমতি দিয়ে।" "গৃহ স্ক্ৰীনত ছেটিং আৰু শ্বৰ্টকাটসমূহ পঢ়া" diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml index 6c1cc468a7..b8d660f415 100644 --- a/res/values-az/strings.xml +++ b/res/values-az/strings.xml @@ -38,6 +38,8 @@ "Tətbiq cütü əlçatan deyil" "Vidceti daşımaq üçün toxunub saxlayın." "Vidceti daşımaq üçün iki dəfə toxunub saxlayın və ya fərdi əməliyyatlardan istifadə edin." + "Digər seçimlər" + "Bütün vidcetləri göstərin" "%1$d × %2$d" "%2$d hündürlük %1$d enində" "%1$s vidceti" @@ -90,6 +92,7 @@ "Quraşdırın" "Tətbiq təklif olunmasın" "Proqnozlaşdırılan tətbiqi bərkidin" + "Qabarcıq" "qısayolları quraşdır" "Tətbiqə istifadəçi müdaxiləsi olmadan qısayolları əlavə etməyə icazə verir." "Əsas səhifə ayarlarını və qısayollarını oxumaq" diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml index 24328cfaee..4d4764e5c1 100644 --- a/res/values-b+sr+Latn/strings.xml +++ b/res/values-b+sr+Latn/strings.xml @@ -38,6 +38,8 @@ "Par aplikacija nije dostupan" "Dodirnite i zadržite radi pomeranja vidžeta." "Dvaput dodirnite i zadržite da biste pomerali vidžet ili koristite prilagođene radnje." + "Još opcija" + "Prikaži sve vidžete" "%1$d×%2$d" "širina od %1$d i visina od %2$d" "%1$s vidžet" @@ -90,6 +92,7 @@ "Instaliraj" "Ne predlaži aplikaciju" "Zakači predviđanje" + "Oblačić" "instaliranje prečica" "Dozvoljava aplikaciji da dodaje prečice bez intervencije korisnika." "čitanje podešavanja i prečica na početnom ekranu" diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml index ebbb378123..641509e219 100644 --- a/res/values-be/strings.xml +++ b/res/values-be/strings.xml @@ -38,6 +38,8 @@ "Спалучэнне праграм недаступнае" "Націсніце і ўтрымлівайце віджэт для перамяшчэння." "Дакраніцеся двойчы і ўтрымлівайце, каб перамясціць віджэт або выкарыстоўваць спецыяльныя дзеянні." + "Дадатковыя параметры" + "Паказваць усе віджэты" "%1$d × %2$d" "Шырына: %1$d, вышыня: %2$d" "Віджэт \"%1$s\"" @@ -90,6 +92,7 @@ "Усталяваць" "Не прапаноўваць праграму" "Замацаваць прапанаваную праграму" + "Бурбалка" "Стварэнне ярлыкоў" "Дазваляе праграмам дадаваць ярлыкі без умяшання карыстальніка." "счытваць налады і ярлыкі на галоўным экране" diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index d5d948eed1..3ce3c5fa36 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -38,6 +38,8 @@ "Двойката приложения не е налице" "Докоснете и задръжте за преместване на приспособление" "Докоснете двукратно и задръжте за преместване на приспособление или използвайте персонал. действия." + "Още опции" + "Показв. на всички присп." "%1$d × %2$d" "Ширина %1$d и височина %2$d" "%1$s приспособление" @@ -90,6 +92,7 @@ "Инсталиране" "Без предлагане на приложение" "Фиксиране на предвиждането" + "Балонче" "инсталиране на преки пътища" "Разрешава на приложението да добавя преки пътища без намеса на потребителя." "четене на настройките и преките пътища на началния екран" diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml index cf75fb58d5..9b23590330 100644 --- a/res/values-bn/strings.xml +++ b/res/values-bn/strings.xml @@ -38,6 +38,8 @@ "অ্যাপ পেয়ার উপলভ্য নেই" "কোনও উইজেট সরাতে সেটি টাচ করে ধরে রাখুন।" "একটি উইজেট সরাতে বা কাস্টম অ্যাকশন ব্যবহার করতে ডবল ট্যাপ করে ধরে রাখুন।" + "আরও বিকল্প" + "সব উইজেট দেখুন" "%1$d × %2$d" "%2$d উচ্চতা অনুযায়ী %1$d প্রস্থ" "%1$sটি উইজেট" @@ -90,6 +92,7 @@ "ইনস্টল করুন" "অ্যাপ সাজেস্ট করবেন না" "আপনার প্রয়োজন হতে পারে এমন অ্যাপ পিন করুন" + "বাবল" "শর্টকাটগুলি ইনস্টল করে" "একটি অ্যাপ্লিকেশানকে ব্যবহারকারীর হস্তক্ষেপ ছাড়াই শর্টকাটগুলি যোগ করার অনুমতি দেয়৷" "হোম স্ক্রিনে সেটিংস ও শর্টকাট পড়ুন" diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml index 1758c39bd8..4a34da7454 100644 --- a/res/values-bs/strings.xml +++ b/res/values-bs/strings.xml @@ -38,6 +38,8 @@ "Par aplikacija nije dostupan" "Dodirnite i zadržite da pomjerite vidžet." "Dvaput dodirnite i zadržite da pomjerite vidžet ili da koristite prilagođene radnje." + "Više opcija" + "Prikazuj sve vidžete" "%1$d × %2$d" "Širina %1$d, visina %2$d" "Vidžet %1$s" @@ -90,6 +92,7 @@ "Instaliraj" "Ne predlaži aplikaciju" "Zakači predviđanje" + "Oblačić" "instaliraj prečice" "Dopušta aplikaciji dodavanje prečica bez posredovanja korisnika." "čita postavke na početnom ekranu i prečice" diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index bf578f5cf1..c341ec73ab 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -38,6 +38,8 @@ "La parella d\'aplicacions no està disponible" "Fes doble toc i mantén premut per moure un widget." "Fes doble toc i mantén premut per moure un widget o per utilitzar accions personalitzades." + "Més opcions" + "Mostra tots els widgets" "%1$d × %2$d" "%1$d d\'amplada per %2$d d\'alçada" "Widget de %1$s" @@ -90,6 +92,7 @@ "Instal·la" "No suggereixis l\'aplicació" "Fixa la predicció" + "Bombolla" "instal·la dreceres" "Permet que una aplicació afegeixi dreceres sense la intervenció de l\'usuari." "llegir la configuració i les dreceres de la pantalla d\'inici" diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 9a8fc6ffb8..d3512c989c 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -38,6 +38,8 @@ "Dvojice aplikací není k dispozici" "Widget přesunete klepnutím a podržením." "Dvojitým klepnutím a podržením přesunete widget, případně použijte vlastní akce." + "Další možnosti" + "Zobrazit všechny widgety" "%1$d × %2$d" "šířka %1$d, výška %2$d" "%1$s widget" @@ -90,6 +92,7 @@ "Nainstalovat" "Nenavrhovat aplikaci" "Připnout předpověď" + "Bublat" "instalace zástupce" "Umožňuje aplikaci přidat zástupce bez zásahu uživatele." "čtení nastavení a zkratek plochy" diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 208c2ef1d6..8aae860908 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -38,6 +38,8 @@ "Appsammenknytning er ikke tilgængelig" "Hold en widget nede for at flytte den." "Tryk to gange, og hold en widget nede for at flytte den eller bruge tilpassede handlinger." + "Flere valgmuligheder" + "Vis alle widgets" "%1$d × %2$d" "%1$d i bredden og %2$d i højden" "Widgetten %1$s" @@ -90,6 +92,7 @@ "Installer" "Foreslå ikke en app" "Fastgør forslaget" + "Boble" "installere genveje" "Tillader, at en app tilføjer genveje uden brugerens indgriben." "læs indstillinger og genveje for startskærm" diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 380030b981..374f5a12a0 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -38,6 +38,8 @@ "App-Paar nicht verfügbar" "Zum Verschieben des Widgets gedrückt halten" "Doppeltippen und halten, um ein Widget zu bewegen oder benutzerdefinierte Aktionen zu nutzen." + "Weitere Optionen" + "Alle Widgets anzeigen" "%1$d × %2$d" "%1$d breit und %2$d hoch" "Widget „%1$s“" @@ -90,6 +92,7 @@ "Installieren" "App nicht vorschlagen" "Vorgeschlagene App fixieren" + "Bubble" "Verknüpfungen installieren" "Ermöglicht einer App das Hinzufügen von Verknüpfungen ohne Eingreifen des Nutzers" "Einstellungen und Verknüpfungen auf dem Startbildschirm lesen" diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index e86ebae4d9..cafe86e407 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -38,6 +38,8 @@ "Το ζεύγος εφαρμογών δεν είναι διαθέσιμο" "Πατήστε παρατετ. για μετακίνηση γραφ. στοιχείου." "Πατήστε δύο φορές παρατεταμένα για μετακίνηση γραφικού στοιχείου ή χρήση προσαρμοσμένων ενεργειών." + "Περισσότερες επιλογές" + "Εμφ. συνόλου γραφ. στοιχ." "%1$d × %2$d" "Πλάτος %1$d επί ύψος %2$d" "Γραφικό στοιχείο %1$s" @@ -90,6 +92,7 @@ "Εγκατάσταση" "Να μην προτείνεται" "Καρφίτσωμα πρόβλεψης" + "Συννεφάκι" "εγκατάσταση συντομεύσεων" "Επιτρέπει σε μια εφαρμογή την προσθήκη συντομεύσεων χωρίς την παρέμβαση του χρήστη." "ανάγνωση ρυθμίσεων και συντομεύσεων αρχικής οθόνης" diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml index 50c597690f..1b0722d549 100644 --- a/res/values-en-rAU/strings.xml +++ b/res/values-en-rAU/strings.xml @@ -38,6 +38,8 @@ "App pair isn\'t available" "Touch and hold to move a widget." "Double-tap & hold to move a widget or use custom actions." + "More options" + "Show all widgets" "%1$d × %2$d" "%1$d wide by %2$d high" "%1$s widget" @@ -90,6 +92,7 @@ "Install" "Don\'t suggest app" "Pin prediction" + "Bubble" "install shortcuts" "Allows an app to add shortcuts without user intervention." "read Home settings and shortcuts" diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml index 08ff6e7f82..de41d2cab3 100644 --- a/res/values-en-rCA/strings.xml +++ b/res/values-en-rCA/strings.xml @@ -38,6 +38,8 @@ "App pair isn\'t available" "Touch and hold to move a widget." "Double-tap and hold to move a widget or use custom actions." + "More options" + "Show all widgets" "%1$d × %2$d" "%1$d wide by %2$d high" "%1$s widget" @@ -90,6 +92,7 @@ "Install" "Don\'t suggest app" "Pin Prediction" + "Bubble" "install shortcuts" "Allows an app to add shortcuts without user intervention." "read home settings and shortcuts" diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index 50c597690f..1b0722d549 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -38,6 +38,8 @@ "App pair isn\'t available" "Touch and hold to move a widget." "Double-tap & hold to move a widget or use custom actions." + "More options" + "Show all widgets" "%1$d × %2$d" "%1$d wide by %2$d high" "%1$s widget" @@ -90,6 +92,7 @@ "Install" "Don\'t suggest app" "Pin prediction" + "Bubble" "install shortcuts" "Allows an app to add shortcuts without user intervention." "read Home settings and shortcuts" diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml index 50c597690f..1b0722d549 100644 --- a/res/values-en-rIN/strings.xml +++ b/res/values-en-rIN/strings.xml @@ -38,6 +38,8 @@ "App pair isn\'t available" "Touch and hold to move a widget." "Double-tap & hold to move a widget or use custom actions." + "More options" + "Show all widgets" "%1$d × %2$d" "%1$d wide by %2$d high" "%1$s widget" @@ -90,6 +92,7 @@ "Install" "Don\'t suggest app" "Pin prediction" + "Bubble" "install shortcuts" "Allows an app to add shortcuts without user intervention." "read Home settings and shortcuts" diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml index fa6d1f1a87..a856340e20 100644 --- a/res/values-en-rXC/strings.xml +++ b/res/values-en-rXC/strings.xml @@ -38,6 +38,8 @@ "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‎‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‏‏‏‎App pair isn\'t available‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‎Touch & hold to move a widget.‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎Double-tap & hold to move a widget or use custom actions.‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‏‎More options‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‏‏‎Show all widgets‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‎%1$d × %2$d‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‎%1$d wide by %2$d high‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ widget‎‏‎‎‏‎" @@ -90,6 +92,7 @@ "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‎‏‎Install‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‎‎‏‏‎‎‎‎‏‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‎‏‎‎‏‎‎Don\'t suggest app‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‏‎‎‏‎‎‏‏‏‎‎‏‎‏‎‏‎‎‏‎‎Pin Prediction‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‎‏‎‏‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‏‎‎Bubble‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‎‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎install shortcuts‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‏‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‏‎‎‎‎‎‎‏‏‎Allows an app to add shortcuts without user intervention.‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‏‏‏‎‏‏‏‎‏‏‏‏‎‏‏‎read home settings and shortcuts‎‏‎‎‏‎" diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml index 5879129996..ba1b0afc69 100644 --- a/res/values-es-rUS/strings.xml +++ b/res/values-es-rUS/strings.xml @@ -38,6 +38,8 @@ "La vinculación de apps no está disponible" "Mantén presionado para mover un widget." "Presiona dos veces y mantén presionado para mover un widget o usar acciones personalizadas." + "Más opciones" + "Mostrar todos los widgets" "%1$d × %2$d" "%1$d de ancho por %2$d de alto" "%1$s widget" @@ -90,6 +92,7 @@ "Instalar" "No sugerir app" "Fijar predicción" + "Burbuja" "instalar accesos directos" "Permite que una aplicación agregue accesos directos sin que el usuario intervenga." "leer parámetros de configuración y accesos directos de la página principal" diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 8d384acba4..ad12192528 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -38,6 +38,8 @@ "La aplicación emparejada no está disponible" "Mantén pulsado un widget para moverlo" "Toca dos veces y mantén pulsado un widget para moverlo o usar acciones personalizadas." + "Más opciones" + "Mostrar todos los widgets" "%1$d × %2$d" "%1$d de ancho por %2$d de alto" "Widget de %1$s" @@ -90,6 +92,7 @@ "Instalar" "No sugerir aplicación" "Fijar predicción" + "Burbuja" "instalar accesos directos" "Permite que una aplicación añada accesos directos sin intervención del usuario." "leer ajustes y accesos directos de la pantalla de inicio" diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index 44448a6b0e..96d0b2c054 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -38,6 +38,8 @@ "Rakendusepaar ei ole saadaval" "Vidina teisaldamiseks puudutage ja hoidke all." "Vidina teisaldamiseks või kohandatud toimingute kasutamiseks topeltpuudutage ja hoidke all." + "Rohkem valikuid" + "Kuva kõik vidinad" "%1$d × %2$d" "%1$d lai ja %2$d kõrge" "Vidin %1$s" @@ -90,6 +92,7 @@ "Installimine" "Ära soovita rakendust" "Kinnita ennustus" + "Mull" "installi otseteed" "Võimaldab rakendusel lisada otseteid kasutaja sekkumiseta." "avakuva seadete ja otseteede lugemine" diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 6fc4cf458f..bc9b8c196c 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -38,6 +38,8 @@ "Aplikazio parea ez dago erabilgarri" "Eduki sakatuta widget bat mugitzeko." "Sakatu birritan eta eduki sakatuta widget bat mugitzeko edo ekintza pertsonalizatuak erabiltzeko." + "Aukera gehiago" + "Erakutsi widget guztiak" "%1$d × %2$d" "%1$d zabal eta %2$d luze" "%1$s widgeta" @@ -90,6 +92,7 @@ "Instalatu" "Ez iradoki aplikazioa" "Ainguratu iragarpena" + "Burbuila" "Instalatu lasterbideak" "Erabiltzaileak ezer egin gabe lasterbideak gehitzeko baimena ematen die aplikazioei." "irakurri hasierako pantailako ezarpenak eta lasterbideak" diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index b1acbafce3..c167194f17 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -38,6 +38,8 @@ "جفت برنامه دردسترس نیست" "برای جابه‌جا کردن ابزاره، لمس کنید و نگه دارید." "برای جابه‌جا کردن ابزاره یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید." + "گزینه‌های بیشتر" + "نمایش همه ابزاره‌ها" "%1$d × %2$d" "‏%1$d عرض در %2$d طول" "ابزاره %1$s" @@ -90,6 +92,7 @@ "نصب" "برنامه پیشنهاد داده نشود" "سنجاق کردن پیشنهاد" + "حبابک" "نصب میان‌برها" "به برنامه اجازه می‌دهد میان‌برها را بدون دخالت کاربر اضافه کند." "خواندن تنظیمات و میان‌برهای صفحه اصلی" diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 310053e557..007d07749e 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -38,6 +38,8 @@ "Sovelluspari ei ole saatavilla" "Kosketa pitkään, niin voit siirtää widgetiä." "Kaksoisnapauta ja paina pitkään, niin voit siirtää widgetiä tai käyttää muokattuja toimintoja." + "Lisäasetukset" + "Näytä kaikki widgetit" "%1$d × %2$d" "Leveys: %1$d, korkeus: %2$d" "%1$s widget" @@ -90,6 +92,7 @@ "Asenna" "Älä ehdota sovellusta" "Kiinnitä sovellus" + "Kupla" "asenna pikakuvakkeita" "Antaa sovelluksen lisätä pikakuvakkeita itsenäisesti ilman käyttäjän valintaa." "lukea aloitusnäytön asetuksia ja pikakuvakkeita" diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml index 11b2c010e5..c443505c28 100644 --- a/res/values-fr-rCA/strings.xml +++ b/res/values-fr-rCA/strings.xml @@ -38,6 +38,8 @@ "La Paire d\'applis n\'est pas offerte" "Maintenez le doigt sur un widget pour le déplacer." "Touchez 2x un widget et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées." + "Autres options" + "Afficher tous les widgets" "%1$d × %2$d" "%1$d de largeur sur %2$d de hauteur" "Widget %1$s" @@ -90,6 +92,7 @@ "Installer" "Ne pas suggérer d\'appli" "Épingler la prédiction" + "Bulle" "installer des raccourcis" "Permet à une appli d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur." "lire les paramètres et les raccourcis de la page d\'accueil" diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 30fbf1763e..4f5d111343 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -38,6 +38,8 @@ "La paire d\'applications n\'est pas disponible" "Appuyez de manière prolongée sur un widget pour le déplacer." "Appuyez deux fois et maintenez la pression pour déplacer widget ou utiliser actions personnalisées." + "Autres options" + "Afficher tous les widgets" "%1$d x %2$d" "%1$d de largeur et %2$d de hauteur" "Widget %1$s" @@ -90,6 +92,7 @@ "Installer" "Ne pas suggérer d\'appli" "Épingler la prédiction" + "Bulle" "installer des raccourcis" "Permettre à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur" "Lire les paramètres et les raccourcis de la page d\'accueil" diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index ac7280218c..ff7c0297c5 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -38,6 +38,8 @@ "Non está dispoñible o emparellamento de aplicacións" "Mantén premido un widget para movelo." "Toca dúas veces un widget e manteno premido para movelo ou utiliza accións personalizadas." + "Máis opcións" + "Mostrar todos os widgets" "%1$d × %2$d" "%1$d de largo por %2$d de alto" "Widget %1$s" @@ -90,6 +92,7 @@ "Instalar" "Non suxerir app" "Fixar predición" + "Burbulla" "instalar atallos" "Permite a unha aplicación engadir atallos sen intervención do usuario." "ler a configuración e os atallos da pantalla de inicio" diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml index 74747d05a6..872faefb9b 100644 --- a/res/values-gu/strings.xml +++ b/res/values-gu/strings.xml @@ -38,6 +38,8 @@ "ઍપની જોડી ઉપલબ્ધ નથી" "વિજેટ ખસેડવા ટચ કરીને થોડી વાર દબાવી રાખો." "વિજેટ ખસેડવા બે વાર ટૅપ કરીને દબાવી રાખો અથવા કસ્ટમ ક્રિયાઓનો ઉપયોગ કરો." + "વધુ વિકલ્પો" + "બધા વિજેટ બતાવો" "%1$d × %2$d" "%1$d પહોળાઈ X %2$d ઊંચાઈ" "%1$s વિજેટ" @@ -90,6 +92,7 @@ "ઇન્સ્ટૉલ કરો" "ઍપ સૂચવશો નહીં" "પૂર્વાનુમાનને પિન કરો" + "બબલ" "શૉર્ટકટ ઇન્સ્ટૉલ કરો" "એપ્લિકેશનને વપરાશકર્તા હસ્તક્ષેપ વગર શોર્ટકટ્સ ઉમેરવાની મંજૂરી આપે છે." "હોમ સેટિંગ અને શૉર્ટકટ વાંચો" diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index 6071935713..a44b8742f5 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -38,6 +38,8 @@ "साथ में इस्तेमाल किए जा सकने वाले ऐप्लिकेशन की सुविधा उपलब्ध नहीं है" "किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उसे दबाकर रखें." "किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उस पर दो बार टैप करके दबाकर रखें या पसंद के मुताबिक कार्रवाइयां इस्तेमाल करें." + "ज़्यादा विकल्प" + "सभी विजेट दिखाएं" "%1$d × %2$d" "%1$d चौड़ाई गुणा %2$d ऊंचाई" "%1$s विजेट" @@ -90,6 +92,7 @@ "इंस्‍टॉल करें" "ऐप्लिकेशन का सुझाव न दें" "सुझाए गए ऐप पिन करें" + "बबल" "शॉर्टकट इंस्‍टॉल करें" "ऐप को उपयोगकर्ता के हस्‍तक्षेप के बिना शॉर्टकट जोड़ने देती है." "होम स्क्रीन की सेटिंग और शॉर्टकट पढ़ने की अनुमति" diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index cf7a91a9db..f62384c048 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -38,6 +38,8 @@ "Par aplikacija nije dostupan" "Dodirnite i zadržite da biste premjestili widget." "Dvaput dodirnite i zadržite pritisak da biste premjestili widget ili upotrijebite prilagođene radnje" + "Više opcija" + "Prikaži sve widgete" "%1$d × %2$d" "%1$d širine i %2$d visine" "Widget %1$s" @@ -90,6 +92,7 @@ "Instaliraj" "Ne predlaži aplikaciju" "Prikvači predviđenu apl." + "Oblačić" "instaliranje prečaca" "Aplikaciji omogućuje dodavanje prečaca bez intervencije korisnika." "čitati postavke i prečace početnog zaslona" @@ -186,7 +189,7 @@ "Filtrirajte" "Nije uspjelo: %1$s" "Privatni prostor" - "Dodirnite da biste postavili ili otvorili" + "Dodirnite za postavljanje ili otvaranje" "Privatno" "Postavke privatnog prostora" "Privatno, otključano." diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index f306110f4f..6bc8b70130 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -38,6 +38,8 @@ "Az alkalmazáspár nem áll rendelkezésre" "Tartsa lenyomva a modult az áthelyezéshez." "Modul áthelyezéséhez koppintson duplán, tartsa nyomva az ujját, vagy használjon egyéni műveleteket." + "További lehetőségek" + "Minden modul mutatása" "%1$d × %2$d" "%1$d széles és %2$d magas" "%1$s modul" @@ -90,6 +92,7 @@ "Telepítés" "Ne javasoljon appot" "Várható kitűzése" + "Buborék" "parancsikonok telepítése" "Lehetővé teszi egy alkalmazás számára, hogy felhasználói beavatkozás nélkül adjon hozzá parancsikonokat." "kezdőképernyő beállításainak és parancsikonjainak olvasása" diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml index 2d345f2931..69b320dc40 100644 --- a/res/values-hy/strings.xml +++ b/res/values-hy/strings.xml @@ -38,6 +38,8 @@ "Հավելվածների զույգը հասանելի չէ" "Հպեք և պահեք՝ վիջեթ տեղափոխելու համար։" "Կրկնակի հպեք և պահեք՝ վիջեթ տեղափոխելու համար, կամ օգտվեք հատուկ գործողություններից։" + "Այլ ընտրանքներ" + "Ցույց տալ բոլոր վիջեթները" "%1$d × %2$d" "Լայնությունը՝ %1$d, բարձրությունը՝ %2$d" "%1$s վիջեթ" @@ -90,6 +92,7 @@ "Տեղադրել" "Չառաջարկել" "Ամրացնել առաջարկվող հավելվածը" + "Ամպիկ" "Դյուրանցումների տեղադրում" "Հավելվածին թույլ է տալիս ավելացնել դյուրանցումներ՝ առանց օգտագործողի միջամտության:" "կարդալ հիմնական էկրանի կարգավորումներն ու դյուրանցումները" diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml index 9ced9f4832..58a429f3f1 100644 --- a/res/values-in/strings.xml +++ b/res/values-in/strings.xml @@ -38,6 +38,8 @@ "Pasangan aplikasi tidak tersedia" "Sentuh lama untuk memindahkan widget." "Ketuk dua kali & tahan untuk memindahkan widget atau gunakan tindakan khusus." + "Opsi lainnya" + "Tampilkan semua widget" "%1$d × %2$d" "lebar %1$d x tinggi %2$d" "Widget %1$s" @@ -90,6 +92,7 @@ "Instal" "Jangan sarankan apl" "Pin Prediksi" + "Balon" "memasang pintasan" "Mengizinkan aplikasi menambahkan pintasan tanpa campur tangan pengguna." "membaca setelan dan pintasan layar utama" diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml index 2ab781732c..95bd21f5d6 100644 --- a/res/values-is/strings.xml +++ b/res/values-is/strings.xml @@ -38,6 +38,8 @@ "Forritapar er ekki í boði" "Haltu fingri á græju til að færa hana." "Ýttu tvisvar og haltu fingri á græju til að færa hana eða notaðu sérsniðnar aðgerðir." + "Fleiri valkostir" + "Sýna allar græjur" "%1$d × %2$d" "%1$d á breidd og %2$d á hæð" "Græjan %1$s" @@ -90,6 +92,7 @@ "Setja upp" "Ekki fá tillögu að forriti" "Festa tillögu" + "Blaðra" "setja upp flýtileiðir" "Leyfir forriti að bæta við flýtileiðum án íhlutunar notanda." "lesa stillingar og flýtileiðir heimaskjás" diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index a59de6c049..3c01cd4b7b 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -38,6 +38,8 @@ "La coppia di app non è disponibile" "Tocca e tieni premuto per spostare un widget." "Tocca due volte e tieni premuto per spostare un widget o per usare le azioni personalizzate." + "Altre opzioni" + "Mostra tutti i widget" "%1$d × %2$d" "%1$d di larghezza per %2$d di altezza" "Widget %1$s" @@ -90,6 +92,7 @@ "Installa" "Non suggerire app" "Blocca previsione" + "Fumetto" "Aggiunta di scorciatoie" "Consente a un\'app di aggiungere scorciatoie automaticamente." "leggere le impostazioni e le scorciatoie nella schermata Home" diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 3d1109527c..f1981662ac 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -38,6 +38,8 @@ "צמד האפליקציות לא זמין" "להעברת ווידג\'ט למקום אחר לוחצים עליו לחיצה ארוכה." "כדי להעביר ווידג\'ט למקום אחר או להשתמש בפעולות מותאמות אישית, יש ללחוץ פעמיים ולא להרפות." + "אפשרויות נוספות" + "הצגת כל הווידג\'טים" "%1$d × %2$d" "‏רוחב %1$d על גובה %2$d" "ווידג\'ט %1$s" @@ -90,6 +92,7 @@ "התקנה" "בלי להציע את האפליקציה" "הצמדת החיזוי" + "בועה" "התקנת קיצורי דרך" "מאפשר לאפליקציה להוסיף קיצורי דרך ללא התערבות המשתמש." "קריאת ההגדרות וקיצורי הדרך בדף הבית" diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index b48c7bea3e..d2f9a97d36 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -38,6 +38,8 @@ "アプリのペア設定は利用できません" "長押ししてウィジェットを移動させます。" "ウィジェットをダブルタップして長押ししながら移動するか、カスタム操作を使用してください。" + "その他のオプション" + "すべてのウィジェットを表示" "%1$dx%2$d" "幅 %1$d、高さ %2$d" "%1$s ウィジェット" @@ -90,6 +92,7 @@ "インストール" "アプリを表示しない" "アプリの候補を固定" + "ふきだし" "ショートカットのインストール" "ユーザー操作なしでショートカットを追加することをアプリに許可します。" "ホームの設定とショートカットの読み取り" diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml index 76b8b5d6c0..e67cc413fe 100644 --- a/res/values-ka/strings.xml +++ b/res/values-ka/strings.xml @@ -38,6 +38,8 @@ "აპთა წყვილი მიუწვდომელია" "შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად." "ორმაგი შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად ან მორგებული მოქმედებების გამოსაყენებლად." + "სხვა ვარიანტები" + "ყველა ვიჯეტის ჩვენება" "%1$d × %2$d" "სიგრძე: %1$d, სიგანე: %2$d" "%1$s ვიჯეტი" @@ -90,6 +92,7 @@ "ინსტალაცია" "არ შემომთავაზო აპი" "ჩამაგრების პროგნოზირება" + "ბუშტი" "მალსახმობების დაყენება" "აპისთვის მალსახმობების დამოუკიდებლად დამატების უფლების მიცემა." "მთავარი ეკრანის პარამეტრებისა და მალსახმობების წაკითხვა" diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml index 95d4420157..d5ccae567d 100644 --- a/res/values-kk/strings.xml +++ b/res/values-kk/strings.xml @@ -38,6 +38,8 @@ "Қолданбаларды жұптау функциясы қолжетімді емес." "Виджетті жылжыту үшін басып тұрыңыз." "Виджетті жылжыту үшін екі рет түртіңіз де, ұстап тұрыңыз немесе арнаулы әрекеттерді пайдаланыңыз." + "Басқа опциялар" + "Барлық виджетті көрсету" "%1$d × %2$d" "Ені: %1$d, биіктігі: %2$d" "%1$s виджеті" @@ -90,6 +92,7 @@ "Орнату" "Қолданба ұсынбау" "Болжамды бекіту" + "Қалқыма терезе" "таңбаша орнату" "Қолданбаға пайдаланушының қатысуынсыз төте пернелерді қосу мүмкіндігін береді." "негізгі экран параметрлері мен таңбашаларын оқу" diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml index 5c71276583..ebd68f78e7 100644 --- a/res/values-km/strings.xml +++ b/res/values-km/strings.xml @@ -38,6 +38,8 @@ "មិនអាចប្រើគូកម្មវិធីបានទេ" "ចុចឱ្យជាប់​ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក​។" "ចុចពីរដង រួចសង្កត់ឱ្យជាប់ ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក ឬប្រើ​សកម្មភាព​តាមបំណង​។" + "ជម្រើស​ច្រើនទៀត" + "បង្ហាញគ្រប់ធាតុ​ក្រាហ្វិក" "%1$d × %2$d" "ទទឺង %1$d គុណនឹងកម្ពស់ %2$d" "ធាតុ​ក្រាហ្វិក %1$s" @@ -90,6 +92,7 @@ "ដំឡើង" "កុំណែនាំកម្មវិធី" "ខ្ទាស់ការ​ព្យាករ" + "ពពុះ" "ដំឡើង​ផ្លូវកាត់" "អនុញ្ញាត​ឲ្យ​កម្មវិធី​បន្ថែម​ផ្លូវកាត់​ ដោយ​មិន​ចាំបាច់​​អំពើ​ពី​អ្នក​ប្រើ។" "អានការកំណត់ និងផ្លូវកាត់របស់អេក្រង់ដើម" @@ -114,7 +117,7 @@ "ថត៖ %1$s, ធាតុ %2$d" "ថត៖ %1$s, ធាតុ %2$d ឬច្រើនជាងនេះ" "គូកម្មវិធី៖ %1$s និង %2$s" - "ផ្ទាំងរូបភាព និងរចនាប័ទ្ម" + "ផ្ទាំងរូបភាព និងរចនាបថ" "កែអេក្រង់ដើម" "ការកំណត់​ទំព័រដើម" "បានបិទដំណើរការដោយអ្នកគ្រប់គ្រងរបស់អ្នក" diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml index 931ca30b58..ab84833c98 100644 --- a/res/values-kn/strings.xml +++ b/res/values-kn/strings.xml @@ -38,6 +38,8 @@ "ಆ್ಯಪ್ ಜೋಡಿ ಲಭ್ಯವಿಲ್ಲ" "ವಿಜೆಟ್ ಸರಿಸಲು ಸ್ಪರ್ಶಿಸಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ." "ವಿಜೆಟ್ ಸರಿಸಲು ಅಥವಾ ಕಸ್ಟಮ್ ಕ್ರಿಯೆಗಳನ್ನು ಬಳಸಲು ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ." + "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" + "ಎಲ್ಲಾ ವಿಜೆಟ್‌ ತೋರಿಸಿ" "%1$d × %2$d" "%1$d ಅಗಲ ಮತ್ತು %2$d ಎತ್ತರ" "%1$s ವಿಜೆಟ್" @@ -90,6 +92,7 @@ "ಸ್ಥಾಪಿಸಿ" "ಆ್ಯಪ್ ಅನ್ನು ಸೂಚಿಸಬೇಡಿ" "ಮುನ್ನೋಟ ಪಿನ್ ಮಾಡಿ" + "ಬಬಲ್" "ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸ್ಥಾಪಿಸಿ" "ಬಳಕೆದಾರರ ಹಸ್ತಕ್ಷೇಪವಿಲ್ಲದೆ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ." "ಹೋಮ್ ಸ್ಕ್ರೀನ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಓದಿ" diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 9f64cfb24e..318cd004f2 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -38,6 +38,8 @@ "앱 페어링을 사용할 수 없습니다." "길게 터치하여 위젯을 이동하세요." "두 번 탭한 다음 길게 터치하여 위젯을 이동하거나 맞춤 작업을 사용하세요." + "옵션 더보기" + "모든 위젯 표시" "%1$d×%2$d" "너비 %1$d, 높이 %2$d" "위젯 %1$s개" @@ -90,6 +92,7 @@ "설치" "앱 제안 받지 않음" "예상 앱 고정" + "풍선" "바로가기 설치" "앱이 사용자의 작업 없이 바로가기를 추가할 수 있도록 합니다." "홈 설정 및 바로가기 읽기" diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml index 3e67c365fb..856e2b22f8 100644 --- a/res/values-ky/strings.xml +++ b/res/values-ky/strings.xml @@ -38,6 +38,8 @@ "Эки колдонмону бир маалда пайдаланууга болбойт" "Виджетти кое бербей басып туруп жылдырыңыз." "Виджетти жылдыруу үчүн эки жолу таптап, кармап туруңуз же ыңгайлаштырылган аракеттерди колдонуңуз." + "Дагы параметрлер" + "Виджеттин баарын көрсөтүү" "%1$d × %2$d" "Туурасы: %1$d, бийиктиги: %2$d" "%1$s виджети" @@ -90,6 +92,7 @@ "Орнотуу" "Cунушталбасын" "Божомолдонгон колдонмону кадап коюу" + "Көбүкчө" "тез чакырмаларды орнотуу" "Колдонмого колдонуучуга кайрылбастан тез чакырма кошууга уруксат берет." "үйдүн параметрлерин жана ыкчам баскычтарын окуу" diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml index e0cd3ee883..3d1a6c95b0 100644 --- a/res/values-lo/strings.xml +++ b/res/values-lo/strings.xml @@ -38,6 +38,8 @@ "ການຈັບຄູ່ແອັບບໍ່ມີໃຫ້" "ແຕະຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ." "ແຕະສອງເທື່ອຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ ຫຼື ໃຊ້ຄຳສັ່ງກຳນົດເອງ." + "ຕົວເລືອກເພີ່ມເຕີມ" + "ສະແດງວິດເຈັດທັງໝົດ" "%1$d × %2$d" "ກວ້າງ %1$d ຄູນສູງ %2$d" "ວິດເຈັດ %1$s" @@ -90,6 +92,7 @@ "ຕິດຕັ້ງ" "ຢ່າແນະນຳແອັບ" "ປັກໝຸດການຄາດເດົາ" + "ຟອງ" "ຕິດຕັ້ງທາງລັດ" "ອະນຸຍາດໃຫ້ແອັບຯ ເພີ່ມທາງລັດໂດຍບໍ່ຕ້ອງຮັບການຢືນຢັນຈາກຜູ່ໃຊ້." "ອ່ານການຕັ້ງຄ່າໜ້າຫຼັກ ແລະ ທາງລັດ" diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml index 9846525aa7..4c9bd9b2e9 100644 --- a/res/values-lt/strings.xml +++ b/res/values-lt/strings.xml @@ -38,6 +38,8 @@ "Programų pora nepasiekiama" "Dukart pal. ir palaik., kad perkeltumėte valdiklį." "Dukart palieskite ir palaikykite, kad perkeltumėte valdiklį ar naudotumėte tinkintus veiksmus." + "Daugiau parinkčių" + "Rodyti visus valdiklius" "%1$d × %2$d" "%1$d plotis ir %2$d aukštis" "%1$s valdiklis" @@ -90,6 +92,7 @@ "Įdiegti" "Nesiūlyti programos" "Prisegti numatymą" + "Debesėlis" "įdiegti sparčiuosius klavišus" "Programai leidžiama pridėti sparčiuosius klavišus be naudotojo įsikišimo." "skaityti pagrindinio ekrano nustatymus ir sparčiuosius klavišus" diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml index 49f7ffe3a4..0a82705148 100644 --- a/res/values-lv/strings.xml +++ b/res/values-lv/strings.xml @@ -38,6 +38,8 @@ "Lietotņu pāris nav pieejams" "Lai pārvietotu logrīku, pieskarieties un turiet." "Lai pārvietotu logrīku, uz tā veiciet dubultskārienu un turiet. Varat arī veikt pielāgotas darbības." + "Citas iespējas" + "Rādīt visus logrīkus" "%1$d × %2$d" "%1$d plats un %2$d augsts" "Logrīks %1$s" @@ -90,6 +92,7 @@ "Instalēt" "Neieteikt lietotni" "Piespraust prognozēto lietotni" + "Burbulis" "instalēt saīsnes" "Ļauj lietotnei pievienot saīsnes, nejautājot lietotājam." "sākuma ekrāna iestatījumu un saīšņu lasīšana" diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml index 2bfa089ad3..887ca82e63 100644 --- a/res/values-mk/strings.xml +++ b/res/values-mk/strings.xml @@ -38,6 +38,8 @@ "Парот апликации не е достапен" "Допрете и задржете за да преместите виџет." "Допрете двапати и задржете за да преместите виџет или користете приспособени дејства." + "Повеќе опции" + "Прикажи ги сите виџети" "%1$d × %2$d" "%1$d широк на %2$d висок" "Виџет %1$s" @@ -90,6 +92,7 @@ "Инсталирај" "Не предлагај апл." "Закачи го предвидувањето" + "Балонче" "инсталирање кратенки" "Овозможува апликацијата да додава кратенки без интервенција на корисникот." "да чита поставки и кратенки на почетна страница" diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml index a2babd510c..dda56797a6 100644 --- a/res/values-ml/strings.xml +++ b/res/values-ml/strings.xml @@ -38,6 +38,8 @@ "ആപ്പ് ജോടി ലഭ്യമല്ല" "വിജറ്റ് നീക്കാൻ സ്‌പർശിച്ച് പിടിക്കുക." "വിജറ്റ് നീക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യൂ, ഹോൾഡ് ചെയ്യൂ അല്ലെങ്കിൽ ഇഷ്‌ടാനുസൃത പ്രവർത്തനങ്ങൾ ഉപയോഗിക്കൂ." + "കൂടുതൽ ഓപ്ഷനുകൾ" + "എല്ലാ വിജറ്റും കാണിക്കുക" "%1$d × %2$d" "%1$d വീതിയും %2$d ഉയരവും" "%1$s വിജറ്റ്" @@ -90,6 +92,7 @@ "ഇൻസ്‌റ്റാൾ ചെയ്യുക" "ആപ്പ് നിർദ്ദേശിക്കേണ്ട" "പ്രവചനം പിൻ ചെയ്യുക" + "ബബിൾ" "കുറുക്കുവഴികൾ ഇൻസ്റ്റാളുചെയ്യുക" "ഉപയോക്തൃ ഇടപെടൽ ഇല്ലാതെ കുറുക്കുവഴികൾ ചേർക്കാൻ അപ്ലിക്കേഷനെ അനുവദിക്കുന്നു." "ഹോം ക്രമീകരണവും കുറുക്കുവഴികളും വായിക്കുക" diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml index 93a1c9c6fa..49d71c27a2 100644 --- a/res/values-mn/strings.xml +++ b/res/values-mn/strings.xml @@ -38,6 +38,8 @@ "Апп хослуулалт боломжгүй байна" "Виджетийг зөөх бол хүрээд, удаан дарна уу." "Виджетийг зөөх эсвэл захиалгат үйлдлийг ашиглахын тулд хоёр товшоод, удаан дарна уу." + "Бусад сонголт" + "Бүх виджетийг харуулах" "%1$d × %2$d" "%1$d өргөн %2$d өндөр" "%1$s жижиг хэрэгсэл" @@ -90,6 +92,7 @@ "Суулгах" "Апп бүү санал болго" "Таамаглалыг бэхлэх" + "Бөмбөлөг" "товчлол суулгах" "Апп нь хэрэглэгчийн оролцоогүйгээр товчлолыг нэмэж чадна" "нүүрний тохиргоо болон товчлолыг унших" diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml index c084fab567..fdf864afc3 100644 --- a/res/values-mr/strings.xml +++ b/res/values-mr/strings.xml @@ -38,6 +38,8 @@ "ॲपची जोडी उपलब्ध नाही" "विजेट हलवण्यासाठी स्पर्श करा आणि धरून ठेवा." "विजेट हलवण्यासाठी किंवा कस्टम कृती वापरण्यासाठी दोनदा टॅप करा आणि धरून ठेवा." + "आणखी पर्याय" + "सर्व विजेट दाखवा" "%1$d × %2$d" "%1$d रूंद बाय %2$d उंच" "%1$s विजेट" @@ -90,6 +92,7 @@ "इंस्टॉल करा" "ॲप सुचवू नका" "पूर्वानुमान पिन करा" + "बबल" "शॉर्टकट इंस्टॉल करा" "वापरकर्ता हस्तक्षेपाशिवाय शॉर्टकट जोडण्यास अ‍ॅप ला अनुमती देते." "होम सेटिंग्ज आणि शॉर्टकट वाचा" diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml index f26e66f738..b86f657233 100644 --- a/res/values-ms/strings.xml +++ b/res/values-ms/strings.xml @@ -38,6 +38,8 @@ "Gandingan apl tidak tersedia" "Sentuh & tahan untuk menggerakkan widget." "Ketik dua kali & tahan untuk menggerakkan widget atau menggunakan tindakan tersuai." + "Lagi pilihan" + "Tunjukkan semua widget" "%1$d × %2$d" "Lebar %1$d kali tinggi %2$d" "Widget %1$s" @@ -90,6 +92,7 @@ "Pasang" "Jangan cadangkan apl" "Sematkan Ramalan" + "Gelembung" "pasang pintasan" "Membenarkan apl menambah pintasan tanpa campur tangan pengguna." "membaca tetapan dan pintasan skrin utama" diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml index 194ece73ba..7e8fd14b2f 100644 --- a/res/values-my/strings.xml +++ b/res/values-my/strings.xml @@ -38,6 +38,8 @@ "အက်ပ်တွဲချိတ်ခြင်းကို မရနိုင်ပါ" "ဝိဂျက်ကို ရွှေ့ရန် တို့ပြီး ဖိထားပါ။" "ဝိဂျက်ကို ရွှေ့ရန် (သို့) စိတ်ကြိုက်လုပ်ဆောင်ချက်များကို သုံးရန် နှစ်ချက်တို့ပြီး ဖိထားပါ။" + "နောက်ထပ် ရွေးစရာများ" + "ဝိဂျက်အားလုံး ပြပါ" "%1$d × %2$d" "အလျား %1$d နှင့် အမြင့် %2$d" "%1$s ဝိဂျက်" @@ -90,6 +92,7 @@ "ထည့်သွင်းရန်" "အက်ပ်အကြံမပြုပါနှင့်" "ခန့်မှန်းချက်ကို ပင်ထိုးရန်" + "ပူဖောင်းကွက်" "ဖြတ်လမ်းလင့်ခ်များ ထည့်သွင်းခြင်း" "အသုံးပြုသူ လုပ်ဆောင်မှုမရှိပဲ အပ်ပလီကေးရှင်းကို အတိုကောက်မှတ်သားမှုများ ပြုလုပ်ခွင့် ပေးခြင်း" "ပင်မဆက်တင်နှင့် ဖြတ်လမ်းလင့်ခ်များ ဖတ်ခြင်း" diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index a9e6c5d6cc..e2be4ffe74 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -31,13 +31,15 @@ "Delt skjerm" "Appinformasjon for %1$s" "Bruksinnstillinger for %1$s" - "Lagre apptilkoblingen" + "Lagre app-paret" "%1$s | %2$s" "Denne apptilkoblingen støttes ikke på denne enheten" "Åpne enheten for å bruke denne apptilkoblingen" "Apptilkoblingen er ikke tilgjengelig" "Trykk og hold for å flytte en modul." "Dobbelttrykk og hold inne for å flytte en modul eller bruke tilpassede handlinger." + "Flere alternativer" + "Vis alle moduler" "%1$d × %2$d" "%1$d bredde x %2$d høyde" "%1$s-modul" @@ -90,6 +92,7 @@ "Installer" "Ikke foreslå app" "Fest forslaget" + "Boble" "installere snarveier" "Gir apper tillatelse til å legge til snarveier uten innblanding fra brukeren." "lese startsideinnstillinger og -snarveier" diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml index 7880c36e85..fa2e59b0e7 100644 --- a/res/values-ne/strings.xml +++ b/res/values-ne/strings.xml @@ -38,6 +38,8 @@ "एप पेयर उपलब्ध छैन" "कुनै विजेट सार्न डबल ट्याप गरेर छोइराख्नुहोस्।" "कुनै विजेट सार्न वा आफ्नो रोजाइका कारबाही प्रयोग गर्न डबल ट्याप गरेर छोइराख्नुहोस्।" + "थप विकल्पहरू" + "सबै विजेटहरू देखाउनुहोस्" "%1$d × %2$d" "%1$d चौडाइ गुणा %2$d उचाइ" "%1$s विजेट" @@ -90,6 +92,7 @@ "स्थापना गर्नुहोस्" "एप सिफारिस नगर्नुहोस्" "सिफारिस गरिएको एप पिन गर्नुहोस्" + "बबल" "सर्टकट स्थापना गर्नेहोस्" "प्रयोगकर्ताको हस्तक्षेप बिना एउटा एपलाई सर्टकटमा थप्नको लागि अनुमति दिनुहोस्।" "होम स्क्रिनका सेटिङ र सर्टकटहरू रिड गर्नुहोस्" diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml index d23f4d13c4..0f630e5860 100644 --- a/res/values-night-v31/colors.xml +++ b/res/values-night-v31/colors.xml @@ -26,6 +26,8 @@ @android:color/system_neutral1_700 @android:color/system_neutral1_100 + + @android:color/system_neutral2_200 @android:color/system_neutral1_100 @@ -56,38 +58,5 @@ @android:color/system_accent1_900 - @android:color/system_accent2_700 - @android:color/system_accent3_700 - @android:color/system_accent1_700 - @android:color/system_accent2_100 - @android:color/system_accent3_100 - @android:color/system_accent1_100 - @android:color/system_accent2_200 - #FFDAD5 - @android:color/system_accent2_900 - @android:color/system_neutral1_900 - @android:color/system_accent3_200 - @android:color/system_accent3_900 - @android:color/system_accent1_200 - @android:color/system_accent2_700 - #930001 - @android:color/system_accent1_900 - @android:color/system_accent1_600 - @android:color/system_accent2_100 - @android:color/system_accent3_700 - @android:color/system_accent3_100 - @android:color/system_accent1_700 - @android:color/system_neutral1_800 - @android:color/system_accent1_100 - @android:color/system_accent2_800 - @android:color/system_accent3_800 - #690001 - @android:color/system_neutral2_200 - @android:color/system_neutral2_400 - @android:color/system_neutral2_700 - @android:color/system_accent1_800 @android:color/system_neutral1_100 - @android:color/system_accent1_200 - @android:color/system_accent2_200 - @android:color/system_accent3_200 \ No newline at end of file diff --git a/res/values-night-v34/colors.xml b/res/values-night-v34/colors.xml index af2811999c..abce763e0a 100644 --- a/res/values-night-v34/colors.xml +++ b/res/values-night-v34/colors.xml @@ -27,4 +27,7 @@ @android:color/system_on_surface_dark @android:color/system_on_surface_variant_dark + + @android:color/system_on_surface_variant_dark + diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 95b3a630ca..887a2a5187 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -1,64 +1,20 @@ - - + + - #3F4759 - #583E5B #0D0E11 - #2B4678 - #DBE2F9 - #FBD7FC - #1B1B1F - #D8E2FF - #BFC6DC - #FFDAD5 - #141B2C - #1B1B1F - #DEBCDF - #29132D - #ADC6FF - #3F4759 - #930001 - #001A41 - #445E91 - #DBE2F9 - #FAF9FD - #44474F - #583E5B - #FBD7FC - #2B4678 - #E3E2E6 - #D8E2FF - #293041 - #402843 - #121316 - #38393C - #690001 - #121316 - #292A2D - #343538 - #C4C6D0 - #72747D - #444746 - #102F60 #E3E2E6 - #1F1F23 - #ADC6FF - #BFC6DC - #DEBCDF - + \ No newline at end of file diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml index c95722fd58..06f0eee50f 100644 --- a/res/values-night/styles.xml +++ b/res/values-night/styles.xml @@ -26,9 +26,64 @@ true - + + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 7b3f5639ae..9271b9651e 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -38,6 +38,8 @@ "App-paar is niet beschikbaar" "Tik en houd vast om een widget te verplaatsen." "Dubbeltik en houd vast om een widget te verplaatsen of aangepaste acties te gebruiken." + "Meer opties" + "Alle widgets tonen" "%1$d × %2$d" "%1$d breed en %2$d hoog" "Widget %1$s" @@ -90,6 +92,7 @@ "Installeren" "Geen app voorstellen" "Voorspelling vastzetten" + "Bubbel" "Snelle links instellen" "Een app toestaan snelkoppelingen toe te voegen zonder tussenkomst van de gebruiker." "instellingen en snelkoppelingen op startscherm lezen" diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml index 33b7664cfb..98d52def40 100644 --- a/res/values-or/strings.xml +++ b/res/values-or/strings.xml @@ -38,6 +38,8 @@ "ଆପ ପେୟାର ଉପଲବ୍ଧ ନାହିଁ" "ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ସ୍ପର୍ଶ କରି ଧରି ରଖନ୍ତୁ।" "ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ଦୁଇଥର-ଟାପ୍ କରି ଧରି ରଖନ୍ତୁ କିମ୍ବା କଷ୍ଟମ୍ କାର୍ଯ୍ୟଗୁଡ଼ିକୁ ବ୍ୟବହାର କରନ୍ତୁ।" + "ଅଧିକ ବିକଳ୍ପ" + "ସମସ୍ତ ୱିଜେଟ ଦେଖାନ୍ତୁ" "%1$d × %2$d" "%1$d ଓସାର ଓ %2$d ଉଚ୍ଚ" "%1$s ୱିଜେଟ୍" @@ -90,6 +92,7 @@ "ଇନଷ୍ଟଲ୍‌ କରନ୍ତୁ" "ଆପ ପରାମର୍ଶ ଦିଅନ୍ତୁ ନାହିଁ" "ପୂର୍ବାନୁମାନକୁ ପିନ୍ କରନ୍ତୁ" + "ବବଲ" "ସର୍ଟକଟ୍‍ ଇନଷ୍ଟଲ୍‌ କରନ୍ତୁ" "ୟୁଜରଙ୍କ ବିନା ହସ୍ତକ୍ଷେପରେ ଶର୍ଟକଟ୍‌ ଯୋଡ଼ିବାକୁ ଆପକୁ ଅନୁମତି ଦିଏ।" "ହୋମ ସେଟିଂସ ଏବଂ ସର୍ଟକଟଗୁଡ଼ିକୁ ପଢ଼ନ୍ତୁ" diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml index fe2676478e..782979e4fe 100644 --- a/res/values-pa/strings.xml +++ b/res/values-pa/strings.xml @@ -38,6 +38,8 @@ "ਐਪ ਜੋੜਾਬੱਧ ਉਪਲਬਧ ਨਹੀਂ ਹੈ" "ਕਿਸੇ ਵਿਜੇਟ ਨੂੰ ਲਿਜਾਉਣ ਲਈ ਸਪਰਸ਼ ਕਰਕੇ ਰੱਖੋ।" "ਵਿਜੇਟ ਲਿਜਾਉਣ ਲਈ ਜਾਂ ਵਿਉਂਂਤੀਆਂ ਕਾਰਵਾਈਆਂ ਵਰਤਣ ਲਈ ਦੋ ਵਾਰ ਟੈਪ ਕਰਕੇ ਦਬਾ ਕੇ ਰੱਖੋ।" + "ਹੋਰ ਵਿਕਲਪ" + "ਸਭ ਵਿਜੇਟ ਦਿਖਾਓ" "%1$d × %2$d" "%1$d ਚੌੜਾਈ ਅਤੇ %2$d ਲੰਬਾਈ" "%1$s ਵਿਜੇਟ" @@ -90,6 +92,7 @@ "ਸਥਾਪਤ ਕਰੋ" "ਐਪ ਦਾ ਸੁਝਾਅ ਨਾ ਦਿਓ" "ਪੂਰਵ-ਅਨੁਮਾਨ ਪਿੰਨ ਕਰੋ" + "ਬਬਲ" "ਸ਼ਾਰਟਕੱਟ ਸਥਾਪਤ ਕਰੋ" "ਇੱਕ ਐਪ ਨੂੰ ਵਰਤੋਂਕਾਰ ਦੇ ਦਖ਼ਲ ਤੋਂ ਬਿਨਾਂ ਸ਼ਾਰਟਕੱਟ ਸ਼ਾਮਲ ਕਰਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।" "ਹੋਮ ਸੈਟਿੰਗਾਂ ਅਤੇ ਸ਼ਾਰਟਕੱਟ ਪੜ੍ਹੋ" diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 587c40fdc7..71e569c9a6 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -38,6 +38,8 @@ "Para aplikacji jest niedostępna" "Naciśnij i przytrzymaj, aby przenieść widżet." "Naciśnij dwukrotnie i przytrzymaj, aby przenieść widżet lub użyć działań niestandardowych." + "Więcej opcji" + "Pokaż wszystkie widżety" "%1$d × %2$d" "Szerokość %1$d, wysokość %2$d" "Widżet %1$s" @@ -90,6 +92,7 @@ "Zainstaluj" "Nie proponuj aplikacji" "Przypnij podpowiedź" + "Dymek" "Instalowanie skrótów" "Pozwala aplikacji dodawać skróty bez interwencji użytkownika." "Odczytuje ustawienia i skróty na ekranie głównym" diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index 67d93deebc..1c44d9bb46 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -38,6 +38,8 @@ "O par de apps não está disponível" "Toque sem soltar para mover um widget." "Toque duas vezes sem soltar para mover um widget ou utilizar ações personalizadas." + "Mais opções" + "Mostrar todos os widgets" "%1$d × %2$d" "%1$d de largura por %2$d de altura" "Widget %1$s" @@ -90,6 +92,7 @@ "Instalar" "Não sugerir app" "Fixar previsão" + "Balão" "instalar atalhos" "Permite a uma app adicionar atalhos sem a intervenção do utilizador." "ler definições e atalhos do ecrã Principal" diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 4302588495..3f44591448 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -38,6 +38,8 @@ "O Par de apps não está disponível" "Toque e pressione para mover um widget." "Toque duas vezes e mantenha a tela pressionada para mover um widget ou usar ações personalizadas." + "Mais opções" + "Mostrar todos os widgets" "%1$d × %2$d" "%1$d de largura por %2$d de altura" "Widget %1$s" @@ -90,6 +92,7 @@ "Instalar" "Não sugerir esse app" "Fixar previsão" + "Balão" "instalar atalhos" "Permite que um app adicione atalhos sem intervenção do usuário." "ler configurações e atalhos da tela inicial" diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index d9e0413491..b37d93b493 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -38,6 +38,8 @@ "Perechea de aplicații nu este disponibilă" "Atinge și ține apăsat pentru a muta un widget." "Atinge de două ori și ține apăsat pentru a muta un widget sau folosește acțiuni personalizate." + "Mai multe opțiuni" + "Afișează toate widgeturile" "%1$d × %2$d" "%1$d lățime și %2$d înălțime" "Widgetul %1$s" @@ -90,6 +92,7 @@ "Instalează" "Nu sugera aplicația" "Fixează predicția" + "Balon" "instalează comenzi rapide" "Permite unei aplicații să adauge comenzi rapide fără intervenția utilizatorului." "citește setările și comenzile rapide de pe ecranul de pornire" diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index b8c633e223..321bf3726b 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -38,6 +38,8 @@ "Одновременное использование двух приложений недоступно" "Чтобы переместить виджет, нажмите на него и удерживайте" "Чтобы использовать специальные действия или перенести виджет, нажмите на него дважды и удерживайте." + "Другие параметры" + "Показать все виджеты" "%1$d x %2$d" "Ширина %1$d, высота %2$d" "Виджет \"%1$s\"" @@ -90,6 +92,7 @@ "Установить" "Не рекомендовать" "Закрепить рекомендацию" + "Подсказка" "Создание ярлыков" "Приложение сможет самостоятельно добавлять ярлыки." "Доступ к данным о настройках и ярлыках на главном экране" diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml index 71efc03311..91c818ae94 100644 --- a/res/values-si/strings.xml +++ b/res/values-si/strings.xml @@ -38,6 +38,8 @@ "යෙදුම් යුගලයක් නොමැත" "විජට් එකක් ගෙන යාමට ස්පර්ශ කර අල්ලා ගෙන සිටින්න." "විජට් එකක් ගෙන යාමට හෝ අභිරුචි ක්‍රියා භාවිත කිරීමට දෙවරක් තට්ටු කර අල්ලා ගෙන සිටින්න." + "තවත් විකල්ප" + "සියලු ම විජට් පෙන්වන්න" "%1$d × %2$d" "පළල %1$d උස %2$d" "%1$s විජට්ටුව" @@ -90,6 +92,7 @@ "ස්ථාපනය කරන්න" "යෙදුම යෝජනා නොකරන්න" "පුරෝකථනය අමුණන්න" + "බුබුළ" "කෙටිමං ස්ථාපනය කරන්න" "පරිශීලක මැදිහත්වීමෙන් තොරව කෙටිමං එක් කිරීමට යෙදුමකට අවසර දෙයි." "මුල් පිටු සැකසීම් සහ කෙටි මං කියවන්න" diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml index 9930427818..eaae7c0a66 100644 --- a/res/values-sk/strings.xml +++ b/res/values-sk/strings.xml @@ -38,6 +38,8 @@ "Pár aplikácií nie je k dispozícii" "Pridržaním presuňte miniaplikáciu." "Dvojitým klepnutím a pridržaním presuňte miniaplikáciu alebo použite vlastné akcie." + "Ďalšie možnosti" + "Zobrazovať všetky miniaplikácie" "%1$d × %2$d" "šírka %1$d, výška %2$d" "Miniaplikácia %1$s" @@ -90,6 +92,7 @@ "Inštalovať" "Nenavrhovať aplikáciu" "Pripnúť predpoveď" + "Bublina" "inštalácia odkazov" "Povoľuje aplikácii pridať odkazy bez zásahu používateľa." "čítanie nastavení a odkazov plochy" diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 5d423ca27b..dccf2f1698 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -38,6 +38,8 @@ "Par aplikacij ni na voljo" "Pridržite pripomoček, da ga premaknete." "Dvakrat se dotaknite pripomočka in ga pridržite, da ga premaknete, ali pa uporabite dejanja po meri." + "Več možnosti" + "Pokaži vse pripomočke" "%1$d × %2$d" "Širina %1$d, višina %2$d" "Pripomoček %1$s" @@ -90,6 +92,7 @@ "Namesti" "Ne predlagaj" "Predvidevanje pripenjanja" + "Mehurček" "namestitev bližnjic" "Aplikaciji dovoli dodajanje bližnjic brez posredovanja uporabnika." "branje nastavitev in bližnjic na začetnem zaslonu" diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml index 8f4133dcf7..84484e8d4e 100644 --- a/res/values-sq/strings.xml +++ b/res/values-sq/strings.xml @@ -38,6 +38,8 @@ "Çifti i aplikacioneve nuk ofrohet" "Prek dhe mbaj shtypur një miniaplikacion për ta zhvendosur." "Trokit dy herë dhe mbaje shtypur një miniapliikacion për ta zhvendosur atë ose për të përdorur veprimet e personalizuara." + "Opsione të tjera" + "Shfaq të gjitha miniapl." "%1$d × %2$d" "%1$d i gjerë me %2$d i lartë" "%1$s miniaplikacion" @@ -90,6 +92,7 @@ "Instalo" "Mos sugjero aplikacion" "Gozhdo parashikimin" + "Flluskë" "instalimi i shkurtoreve" "Lejon një aplikacion të shtojë shkurtore pa ndërhyrjen e përdoruesit." "lexo cilësimet dhe shkurtoret e ekranit bazë" diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index cd6523ccb9..64383f2df9 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -38,6 +38,8 @@ "Пар апликација није доступан" "Додирните и задржите ради померања виџета." "Двапут додирните и задржите да бисте померали виџет или користите прилагођене радње." + "Још опција" + "Прикажи све виџете" "%1$d×%2$d" "ширина од %1$d и висина од %2$d" "%1$s виџет" @@ -90,6 +92,7 @@ "Инсталирај" "Не предлажи апликацију" "Закачи предвиђање" + "Облачић" "инсталирање пречица" "Дозвољава апликацији да додаје пречице без интервенције корисника." "читање подешавања и пречица на почетном екрану" diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 17e5e6da6d..47aed863bf 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -38,6 +38,8 @@ "App-paret är inte tillgängligt" "Tryck länge för att flytta en widget." "Tryck snabbt två gånger och håll kvar för att flytta en widget eller använda anpassade åtgärder." + "Fler alternativ" + "Visa alla widgetar" "%1$d × %2$d" "%1$d bred gånger %2$d hög" "Widget för %1$s" @@ -90,6 +92,7 @@ "Installera" "Föreslå inte app" "Fäst förslag" + "Bubbla" "installera genvägar" "Tillåter att en app lägger till genvägar utan åtgärd från användaren." "läsa inställningar och genvägar på startskärmen" diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml index 53a9abe056..5eadd1dd3f 100644 --- a/res/values-sw/strings.xml +++ b/res/values-sw/strings.xml @@ -38,6 +38,8 @@ "Kipengele cha jozi ya programu hakipatikani" "Gusa na ushikilie ili usogeze wijeti." "Gusa mara mbili na ushikilie ili usogeze wijeti au utumie vitendo maalum." + "Chaguo zaidi" + "Onyesha wijeti zote" "%1$d × %2$d" "Upana wa %1$d na kimo cha %2$d" "Wijeti ya %1$s" @@ -90,6 +92,7 @@ "Sakinisha" "Isipendekeze programu" "Bandika Utabiri" + "Kiputo" "kuweka njia za mkato" "Huruhusu programu kuongeza njia za mkato bila mtumiaji kuingilia kati." "kusoma mipangilio ya skrini ya kwanza na njia za mkato" diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml index 738f85cafe..d84485ae93 100644 --- a/res/values-ta/strings.xml +++ b/res/values-ta/strings.xml @@ -38,6 +38,8 @@ "ஆப்ஸ் ஜோடி கிடைக்கவில்லை" "விட்ஜெட்டை நகர்த்தத் தொட்டுப் பிடிக்கவும்." "விட்ஜெட்டை நகர்த்த இருமுறை தட்டிப் பிடிக்கவும் அல்லது பிரத்தியேகச் செயல்களைப் பயன்படுத்தவும்." + "கூடுதல் விருப்பங்கள்" + "விட்ஜெட்டுகளைக் காட்டு" "%1$d × %2$d" "%1$d அகலத்திற்கு %2$d உயரம்" "%1$s விட்ஜெட்" @@ -90,6 +92,7 @@ "நிறுவு" "பரிந்துரைக்காதே" "கணிக்கப்பட்டதைப் பின் செய்" + "குமிழ்" "குறுக்குவழிகளை நிறுவுதல்" "பயனரின் அனுமதி இல்லாமல் குறுக்குவழிகளைச் சேர்க்கப் ஆப்ஸை அனுமதிக்கிறது." "முகப்புத் திரையின் அமைப்புகளையும் ஷார்ட்கட்களையும் படித்தல்" diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml index 0520ebf65e..8487e96b1e 100644 --- a/res/values-te/strings.xml +++ b/res/values-te/strings.xml @@ -38,6 +38,8 @@ "యాప్ పెయిర్ అందుబాటులో లేదు" "విడ్జెట్‌ను తరలించడానికి తాకి & నొక్కి ఉంచండి." "విడ్జెట్‌ను తరలించడానికి లేదా అనుకూల చర్యలను ఉపయోగించడానికి రెండుసార్లు నొక్కండి & హోల్డ్ చేయి." + "మరిన్ని ఆప్షన్‌లు" + "అన్ని విడ్జెట్‌లను చూడండి" "%1$d × %2$d" "%1$d వెడల్పు X %2$d ఎత్తు" "%1$s విడ్జెట్" @@ -90,6 +92,7 @@ "ఇన్‌స్టాల్ చేయండి" "యాప్ సూచించకు" "సూచనను పిన్ చేయండి" + "బబుల్" "షార్ట్‌కట్‌లను ఇన్‌స్టాల్ చేయడం" "వినియోగదారు ప్రమేయం లేకుండా షార్ట్‌కట్‌లను జోడించడానికి యాప్‌ను అనుమతిస్తుంది." "హోమ్ సెట్టింగ్‌లు, షార్ట్‌కట్‌లను చదవండి" diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml index 554fd94e5f..71f4d1546b 100644 --- a/res/values-th/strings.xml +++ b/res/values-th/strings.xml @@ -38,6 +38,8 @@ "การจับคู่อุปกรณ์ไม่พร้อมให้บริการ" "แตะค้างไว้เพื่อย้ายวิดเจ็ต" "แตะสองครั้งค้างไว้เพื่อย้ายวิดเจ็ตหรือใช้การดำเนินการที่กำหนดเอง" + "ตัวเลือกเพิ่มเติม" + "แสดงวิดเจ็ตทั้งหมด" "%1$d × %2$d" "กว้าง %1$d x สูง %2$d" "วิดเจ็ต %1$s" @@ -90,6 +92,7 @@ "ติดตั้ง" "ไม่ต้องแนะนำแอป" "ปักหมุดแอปที่คาดการณ์ไว้" + "บับเบิล" "ติดตั้งทางลัด" "อนุญาตให้แอปเพิ่มทางลัดโดยไม่ต้องให้ผู้ใช้จัดการ" "อ่านการตั้งค่าและทางลัดในหน้าแรก" diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml index cb6fe66f8f..7cf6a44f8f 100644 --- a/res/values-tl/strings.xml +++ b/res/values-tl/strings.xml @@ -38,6 +38,8 @@ "Hindi available ang pares ng app" "Pindutin nang matagal para ilipat ang widget." "I-double tap at pindutin nang matagal para ilipat ang widget o gumamit ng mga custom na pagkilos." + "Higit pang opsyon" + "Ipakita lahat ng widget" "%1$d × %2$d" "%1$d ang lapad at %2$d ang taas" "%1$s widget" @@ -90,6 +92,7 @@ "I-install" "Huwag magmungkahi" "I-pin ang Hula" + "Bubble" "i-install ang mga shortcut" "Pinapayagan ang isang app na magdagdag ng mga shortcut nang walang panghihimasok ng user." "basahin ang mga setting at shortcut ng home" diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index b12ec27378..d55181c876 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -38,6 +38,8 @@ "Uygulama çifti kullanılamıyor" "Widget\'ı taşımak için dokunup basılı tutun." "Widget\'ı taşımak veya özel işlemleri kullanmak için iki kez dokunup basılı tutun." + "Diğer seçenekler" + "Tüm widget\'ları göster" "%1$d × %2$d" "genişlik: %1$d, yükseklik: %2$d" "%1$s widget\'ı" @@ -90,6 +92,7 @@ "Yükle" "Uygulamayı önerme" "Tahmini Sabitle" + "Balon" "kısayolları yükle" "Uygulamaya, kullanıcı müdahalesi olmadan kısayol ekleme izni verir." "ana ekran ayarlarını ve kısayollarını oku" diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 2a01daeb5c..b5c1c70b28 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -38,6 +38,8 @@ "Одночасне використання двох додатків недоступне" "Натисніть і втримуйте, щоб перемістити віджет." "Двічі натисніть і втримуйте віджет, щоб перемістити його або виконати інші дії." + "Інші опції" + "Показати всі віджети" "%1$d × %2$d" "Ширина – %1$d, висота – %2$d" "Віджет %1$s" @@ -90,6 +92,7 @@ "Установити" "Не пропонувати додаток" "Закріпити передбачений додаток" + "Повідомлення" "створення ярликів" "Дозволяє програмі самостійно додавати ярлики." "читати налаштування та ярлики головного екрана" diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml index 544357e96a..6fa76dde93 100644 --- a/res/values-ur/strings.xml +++ b/res/values-ur/strings.xml @@ -38,6 +38,8 @@ "ایپ کا جوڑا دستیاب نہیں ہے" "ویجیٹ منتقل کرنے کے لیے ٹچ کریں اور پکڑ کر رکھیں۔" "ویجیٹ کو منتقل کرنے یا حسب ضرورت کارروائیاں استعمال کرنے کے لیے دوبار تھپتھپائیں اور پکڑ کر رکھیں۔" + "مزید اختیارات" + "سبھی ویجیٹس دکھائیں" "%1$d × %2$d" "‏%1$d چوڑا اور ‎%2$d اونچا" "%1$s ویجیٹ" @@ -90,6 +92,7 @@ "انسٹال کریں" "ایپ تجویز نہ کریں" "پیشگوئی پن کریں" + "بلبلہ" "شارٹ کٹس انسٹال کریں" "کسی ایپ کو صارف کی مداخلت کے بغیر شارٹ کٹس شامل کرنے کی اجازت دیتا ہے۔" "ہوم ترتیبات اور شارٹ کٹس کو پڑھیں" diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml index 5038d4f6ab..21a8145a5d 100644 --- a/res/values-uz/strings.xml +++ b/res/values-uz/strings.xml @@ -38,6 +38,8 @@ "Ikkita ilovadan bir vaqtda foydalanish mumkin emas" "Vidjetni bosib turgan holatda suring." "Ikki marta bosib va bosib turgan holatda vidjetni tanlang yoki maxsus amaldan foydalaning." + "Yana" + "Barcha vidjetlar" "%1$d × %2$d" "Eni %1$d, bo‘yi %2$d" "%1$s ta vidjet" @@ -90,6 +92,7 @@ "O‘rnatish" "Tavsiya qilinmasin" "Tavsiyani mahkamlash" + "Pufaklar" "yorliqlar yaratish" "Ilovalarga foydalanuvchidan so‘ramasdan yorliqlar qo‘shishga ruxsat beradi." "Bosh ekrandagi sozlamalar va yorliqlarni koʻrish" diff --git a/res/values-v30/styles.xml b/res/values-v30/styles.xml index ec5c113b85..a5c57c875f 100644 --- a/res/values-v30/styles.xml +++ b/res/values-v30/styles.xml @@ -29,5 +29,6 @@ always false false + #FFFFFFFF diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml index fa87221f52..a5cdfc7a1d 100644 --- a/res/values-v31/colors.xml +++ b/res/values-v31/colors.xml @@ -75,6 +75,8 @@ @android:color/system_neutral1_900 + + @android:color/system_neutral2_700 @android:color/system_neutral1_900 @@ -109,38 +111,5 @@ @android:color/system_neutral1_1000 - @android:color/system_accent2_700 - @android:color/system_accent3_700 - @android:color/system_accent1_700 - @android:color/system_accent2_900 - @android:color/system_accent3_900 - @android:color/system_accent1_900 - @android:color/system_accent2_200 - #410000 - @android:color/system_accent2_900 - @android:color/system_neutral1_100 - @android:color/system_accent3_200 - @android:color/system_accent3_900 - @android:color/system_accent1_200 - @android:color/system_accent2_100 - #FFDAD5 - @android:color/system_accent1_900 - @android:color/system_accent1_200 - @android:color/system_accent2_100 - @android:color/system_accent3_100 - @android:color/system_accent3_100 - @android:color/system_accent1_100 - @android:color/system_neutral1_50 - @android:color/system_accent1_100 - @android:color/system_accent2_0 - @android:color/system_accent3_0 - #FFFFFF - @android:color/system_neutral2_700 - @android:color/system_neutral2_500 - @android:color/system_neutral2_200 - @android:color/system_accent1_0 @android:color/system_neutral1_900 - @android:color/system_accent1_600 - @android:color/system_accent2_600 - @android:color/system_accent3_600 diff --git a/res/values-v33/style.xml b/res/values-v33/style.xml deleted file mode 100644 index 1261b232cc..0000000000 --- a/res/values-v33/style.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/values-v34/colors.xml b/res/values-v34/colors.xml index 26d3712bb1..4f3a769a61 100644 --- a/res/values-v34/colors.xml +++ b/res/values-v34/colors.xml @@ -27,4 +27,108 @@ @android:color/system_on_surface_light @android:color/system_on_surface_variant_light + + @android:color/system_on_surface_variant_light + + + @android:color/system_primary_container_light + @android:color/system_on_primary_container_light + @android:color/system_primary_light + @android:color/system_on_primary_light + @android:color/system_secondary_container_light + @android:color/system_on_secondary_container_light + @android:color/system_secondary_light + @android:color/system_on_secondary_light + @android:color/system_tertiary_container_light + @android:color/system_on_tertiary_container_light + @android:color/system_tertiary_light + @android:color/system_on_tertiary_light + @android:color/system_background_light + @android:color/system_on_background_light + @android:color/system_surface_light + @android:color/system_on_surface_light + @android:color/system_surface_container_low_light + @android:color/system_surface_container_lowest_light + @android:color/system_surface_container_light + @android:color/system_surface_container_high_light + @android:color/system_surface_container_highest_light + @android:color/system_surface_bright_light + @android:color/system_surface_dim_light + @android:color/system_surface_variant_light + @android:color/system_on_surface_variant_light + @android:color/system_outline_light + @android:color/system_outline_variant_light + @android:color/system_error_light + @android:color/system_on_error_light + @android:color/system_error_container_light + @android:color/system_on_error_container_light + @android:color/system_control_activated_light + @android:color/system_control_normal_light + @android:color/system_control_highlight_light + @android:color/system_text_primary_inverse_light + @android:color/system_text_secondary_and_tertiary_inverse_light + @android:color/system_text_primary_inverse_disable_only_light + @android:color/system_text_secondary_and_tertiary_inverse_disabled_light + @android:color/system_text_hint_inverse_light + @android:color/system_palette_key_color_primary_light + @android:color/system_palette_key_color_secondary_light + @android:color/system_palette_key_color_tertiary_light + @android:color/system_palette_key_color_neutral_light + @android:color/system_palette_key_color_neutral_variant_light + @android:color/system_primary_container_dark + @android:color/system_on_primary_container_dark + @android:color/system_primary_dark + @android:color/system_on_primary_dark + @android:color/system_secondary_container_dark + @android:color/system_on_secondary_container_dark + @android:color/system_secondary_dark + @android:color/system_on_secondary_dark + @android:color/system_tertiary_container_dark + @android:color/system_on_tertiary_container_dark + @android:color/system_tertiary_dark + @android:color/system_on_tertiary_dark + @android:color/system_background_dark + @android:color/system_on_background_dark + @android:color/system_surface_dark + @android:color/system_on_surface_dark + @android:color/system_surface_container_low_dark + @android:color/system_surface_container_lowest_dark + @android:color/system_surface_container_dark + @android:color/system_surface_container_high_dark + @android:color/system_surface_container_highest_dark + @android:color/system_surface_bright_dark + @android:color/system_surface_dim_dark + @android:color/system_surface_variant_dark + @android:color/system_on_surface_variant_dark + @android:color/system_outline_dark + @android:color/system_outline_variant_dark + @android:color/system_error_dark + @android:color/system_on_error_dark + @android:color/system_error_container_dark + @android:color/system_on_error_container_dark + @android:color/system_control_activated_dark + @android:color/system_control_normal_dark + @android:color/system_control_highlight_dark + @android:color/system_text_primary_inverse_dark + @android:color/system_text_secondary_and_tertiary_inverse_dark + @android:color/system_text_primary_inverse_disable_only_dark + @android:color/system_text_secondary_and_tertiary_inverse_disabled_dark + @android:color/system_text_hint_inverse_dark + @android:color/system_palette_key_color_primary_dark + @android:color/system_palette_key_color_secondary_dark + @android:color/system_palette_key_color_tertiary_dark + @android:color/system_palette_key_color_neutral_dark + @android:color/system_palette_key_color_neutral_variant_dark + @android:color/system_primary_fixed + @android:color/system_primary_fixed_dim + @android:color/system_on_primary_fixed + @android:color/system_on_primary_fixed_variant + @android:color/system_secondary_fixed + @android:color/system_secondary_fixed_dim + @android:color/system_on_secondary_fixed + @android:color/system_on_secondary_fixed_variant + @android:color/system_tertiary_fixed + @android:color/system_tertiary_fixed_dim + @android:color/system_on_tertiary_fixed + @android:color/system_on_tertiary_fixed_variant diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 471feddb2a..b0bac73214 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -38,6 +38,8 @@ "Hiện không có cặp ứng dụng này" "Chạm và giữ để di chuyển một tiện ích." "Nhấn đúp và giữ để di chuyển một tiện ích hoặc sử dụng các thao tác tùy chỉnh." + "Lựa chọn khác" + "Hiện tất cả tiện ích" "%1$d × %2$d" "Rộng %1$d x cao %2$d" "Tiện ích %1$s" @@ -90,6 +92,7 @@ "Cài đặt" "Không gợi ý ứng dụng" "Ghim ứng dụng dự đoán" + "Bong bóng" "cài đặt lối tắt" "Cho phép ứng dụng thêm lối tắt mà không cần sự can thiệp của người dùng." "đọc lối tắt và các chế độ cài đặt trên màn hình chính" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 7a761585f4..112b9454e6 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -38,6 +38,8 @@ "应用对不可用" "轻触并按住即可移动微件。" "点按两次并按住微件即可移动该微件或使用自定义操作。" + "更多选项" + "显示所有微件" "%1$d × %2$d" "宽 %1$d,高 %2$d" "“%1$s”微件" @@ -90,6 +92,7 @@ "安装" "不要推荐此应用" "固定预测的应用" + "气泡框" "安装快捷方式" "允许应用自行添加快捷方式。" "读取主屏幕设置和快捷方式" diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index ffd9e0b80c..e63093e84c 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -38,6 +38,8 @@ "應用程式配對無法使用" "輕觸並按住即可移動小工具。" "㩒兩下之後㩒住,就可以郁小工具或者用自訂操作。" + "更多選項" + "顯示所有小工具" "%1$d × %2$d" "%1$d 闊,%2$d 高" "「%1$s」小工具" @@ -90,6 +92,7 @@ "安裝" "不要提供應用程式建議" "固定預測" + "氣泡" "安裝捷徑" "允許應用程式無需使用者許可也可新增捷徑。" "讀取主畫面設定和捷徑" diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index 264d6079bf..25f9703f4c 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -38,6 +38,8 @@ "這個應用程式配對無法使用" "按住即可移動小工具。" "輕觸兩下並按住即可移動小工具或使用自訂操作。" + "更多選項" + "顯示所有小工具" "%1$d × %2$d" "寬度為 %1$d,高度為 %2$d" "「%1$s」小工具" @@ -90,6 +92,7 @@ "安裝" "不要建議此應用程式" "固定預測的應用程式" + "泡泡" "安裝捷徑" "允許應用程式自動新增捷徑。" "讀取主畫面設定和捷徑" diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml index be6cd493fe..ec1f941cdf 100644 --- a/res/values-zu/strings.xml +++ b/res/values-zu/strings.xml @@ -38,6 +38,8 @@ "Ukubhangqwa kwe-app akutholakali" "Thinta uphinde ubambe ukuze uhambise iwijethi." "Thepha kabili uphinde ubambe ukuze uhambise iwijethi noma usebenzise izindlela ezingokwezifiso." + "Okungakhethwa kukho okuningi" + "Bonisa wonke amawijethi" "%1$d × %2$d" "%1$d ububanzi ngokungu-%2$d ukuya phezulu" "Iwijethi elingu-%1$s" @@ -90,6 +92,7 @@ "Faka" "Ungaphakamisi uhlelo lokusebenza" "Ukubikezela Iphinikhodi" + "Ibhamuza" "faka izinqamuleli" "Ivumela uhlelo lokusebenza ukufaka izinqamuleli ngaphandle kokungenelela komsebenzisi." "funda amasethingi wasekhaya nezinqamuleli" diff --git a/res/values/attrs.xml b/res/values/attrs.xml index be8b2e13d9..57c9bc7a8f 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -25,7 +25,6 @@ - @@ -45,6 +44,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -61,6 +109,8 @@ + + @@ -580,51 +630,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/values/colors.xml b/res/values/colors.xml index ce8096404b..3f8bede23d 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -17,8 +17,7 @@ ** limitations under the License. */ --> - + #FFC1C1C1 @@ -104,6 +103,8 @@ #EFEDED #FAF9F8 #1F1F1F + #4C4D50 + @color/system_on_surface_variant_light #1F1F1F #444746 #C2E7FF @@ -117,12 +118,14 @@ #C4C7C5 #0B57D0 #0B57D0 - @color/material_color_on_surface - @color/material_color_on_surface_variant + @color/system_on_surface_light + @color/system_on_surface_variant_light #1F2020 #393939 #E3E3E3 + #CCCDCF + @color/system_on_surface_variant_dark #E3E3E3 #C4C7C5 #004A77 @@ -136,51 +139,110 @@ #444746 #062E6F #FFFFFF - @color/material_color_on_surface - @color/material_color_on_surface_variant + @color/system_on_surface_dark + @color/system_on_surface_variant_dark - #3F4759 - #583E5B #FFFFFF - #2B4678 - #141B2C - #29132D - #F5F3F7 - #001A41 - #BFC6DC - #410000 - #141B2C - #E3E2E6 - #DEBCDF - #29132D - #ADC6FF - #DBE2F9 - #FFDAD5 - #001A41 - #ADC6FF - #DBE2F9 - #121316 - #E1E2EC - #FBD7FC - #FBD7FC - #D8E2FF - #1B1B1F - #D8E2FF - #FFFFFF - #FFFFFF - #DBD9DD - #FAF9FD - #FFFFFF - #FAF9FD - #E9E7EC - #E3E2E6 - #44474F - #72747D - #C4C7C5 - #FFFFFF #1B1B1F - #EFEDF1 - #445E91 - #575E71 - #715573 + + #D9E2FF + #001945 + #475D92 + #FFFFFF + #DCE2F9 + #151B2C + #575E71 + #FFFFFF + #FDD7FA + #2A122C + #725572 + #FFFFFF + #FAF8FF + #1A1B20 + #FAF8FF + #1A1B20 + #F4F3FA + #FFFFFF + #EEEDF4 + #E8E7EF + #E2E2E9 + #FAF8FF + #DAD9E0 + #E1E2EC + #44464F + #757780 + #C5C6D0 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #D9E2FF + #44464F + #000000 + #E2E2E9 + #C5C6D0 + #E2E2E9 + #E2E2E9 + #E2E2E9 + #6076AC + #70778B + #8C6D8C + #76777D + #757780 + #2F4578 + #D9E2FF + #B0C6FF + #152E60 + #404659 + #DCE2F9 + #C0C6DC + #2A3042 + #593D59 + #FDD7FA + #E0BBDD + #412742 + #121318 + #E2E2E9 + #121318 + #E2E2E9 + #1A1B20 + #0C0E13 + #1E1F25 + #282A2F + #33343A + #38393F + #121318 + #44464F + #C5C6D0 + #8F9099 + #44464F + #FFB4AB + #690005 + #93000A + #FFDAD6 + #2F4578 + #C5C6D0 + #FFFFFF + #1A1B20 + #44464F + #1A1B20 + #1A1B20 + #1A1B20 + #6076AC + #70778B + #8C6D8C + #76777D + #757780 + #D9E2FF + #B0C6FF + #001945 + #2F4578 + #DCE2F9 + #C0C6DC + #151B2C + #404659 + #FDD7FA + #E0BBDD + #2A122C + #593D59 diff --git a/res/values/config.xml b/res/values/config.xml index 2a3b588245..701e64a4b4 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -67,7 +67,6 @@ - @@ -77,6 +76,7 @@ + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 05724e2a85..f8c075fa5f 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -81,6 +81,11 @@ 32dp 19dp + 5dp + 14sp + -10dp + 20sp + 8dp @@ -308,6 +311,7 @@ 2dp 4dp 2dp + 0.5dp 8dp @@ -457,6 +461,7 @@ 0dp 0dp + 0dp 0dp 0dp 0dp @@ -483,8 +488,6 @@ 24dp 60dp 8dp - 96dp - 48dp 16dp 14dp diff --git a/res/values/strings.xml b/res/values/strings.xml index d33adc41f3..9d0602169c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -45,6 +45,9 @@ App info for %1$s Usage settings for %1$s + + New Window + Save app pair @@ -61,6 +64,12 @@ Touch & hold to move a widget. Double-tap & hold to move a widget or use custom actions. + + More options + + Show all widgets %1$d \u00d7 %2$d @@ -209,7 +218,8 @@ Don\'t suggest app Pin Prediction - + + Bubble diff --git a/res/values/styles.xml b/res/values/styles.xml index ae3d3b3e11..728c523542 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -17,7 +17,7 @@ */ --> - + - + + + + diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 876b6436fb..76c0f90e74 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -74,7 +74,8 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_TASKBAR_ALL_APPS, TYPE_ADD_TO_HOME_CONFIRMATION, TYPE_TASKBAR_OVERLAY_PROXY, - TYPE_TASKBAR_PINNING_POPUP + TYPE_TASKBAR_PINNING_POPUP, + TYPE_PIN_IME_POPUP }) @Retention(RetentionPolicy.SOURCE) public @interface FloatingViewType {} @@ -138,9 +139,6 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_TOUCH_CONTROLLER_NO_INTERCEPT = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER & ~TYPE_TASKBAR_OVERLAYS; - public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP - & ~TYPE_PIN_IME_POPUP; - protected boolean mIsOpen; public AbstractFloatingView(Context context, AttributeSet attrs) { diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index 633091d87b..fec94fe8d7 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -109,11 +109,6 @@ public abstract class BaseActivity extends Activity implements ActivityContext { */ public static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 4; - /** - * State flag indicating if the user will be active shortly. - */ - public static final int ACTIVITY_STATE_USER_WILL_BE_ACTIVE = 1 << 5; - /** * State flag indicating that a state transition is in progress */ @@ -316,7 +311,6 @@ public abstract class BaseActivity extends Activity implements ActivityContext { */ public void setResumed() { addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE); - removeActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE); } public boolean isUserActive() { diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java index 8585b66554..177b28c80f 100644 --- a/src/com/android/launcher3/BaseDraggingActivity.java +++ b/src/com/android/launcher3/BaseDraggingActivity.java @@ -140,15 +140,11 @@ public abstract class BaseDraggingActivity extends BaseActivity } protected void onDeviceProfileInitiated() { - if (mDeviceProfile.isVerticalBarLayout()) { - mDeviceProfile.updateIsSeascape(this); - } } @Override public void onDisplayInfoChanged(Context context, Info info, int flags) { if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.isVerticalBarLayout()) { - mDeviceProfile.updateIsSeascape(this); reapplyUi(); } } diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 7d09164feb..8121e53542 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import static android.graphics.fonts.FontStyle.FONT_WEIGHT_BOLD; +import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL; import static android.text.Layout.Alignment.ALIGN_NORMAL; import static com.android.launcher3.Flags.enableCursorHoverStates; @@ -40,11 +42,15 @@ import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.icu.text.MessageFormat; +import android.text.Spannable; +import android.text.SpannableString; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; +import android.text.style.ImageSpan; import android.util.AttributeSet; +import android.util.Log; import android.util.Property; import android.util.Size; import android.util.TypedValue; @@ -54,6 +60,7 @@ import android.view.View; import android.view.ViewDebug; import android.widget.TextView; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; @@ -82,7 +89,7 @@ import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.IconLabelDotView; +import com.android.launcher3.views.FloatingIconViewCompanion; import java.text.NumberFormat; import java.util.HashMap; @@ -94,7 +101,9 @@ import java.util.Locale; * too aggressive. */ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, - IconLabelDotView, DraggableView, Reorderable { + FloatingIconViewCompanion, DraggableView, Reorderable { + + public static final String TAG = "BubbleTextView"; public static final int DISPLAY_WORKSPACE = 0; public static final int DISPLAY_ALL_APPS = 1; @@ -111,6 +120,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private static final String EMPTY = ""; private static final StringMatcherUtility.StringMatcher MATCHER = StringMatcherUtility.StringMatcher.getInstance(); + private static final int BOLD_TEXT_ADJUSTMENT = FONT_WEIGHT_BOLD - FONT_WEIGHT_NORMAL; private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; @@ -166,7 +176,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @ViewDebug.ExportedProperty(category = "launcher") private boolean mSkipUserBadge = false; @ViewDebug.ExportedProperty(category = "launcher") - private boolean mIsIconVisible = true; + protected boolean mIsIconVisible = true; @ViewDebug.ExportedProperty(category = "launcher") private int mTextColor; @ViewDebug.ExportedProperty(category = "launcher") @@ -430,10 +440,21 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, setDownloadStateContentDescription(info, info.getProgressLevel()); } + /** + * Directly set the icon and label. + */ + @UiThread + public void applyIconAndLabel(Drawable icon, CharSequence label) { + applyCompoundDrawables(icon); + setText(label); + setContentDescription(label); + } + /** Updates whether the app this view represents is currently running. */ @UiThread public void updateRunningState(RunningAppState runningAppState) { mRunningAppState = runningAppState; + invalidate(); } protected void setItemInfo(ItemInfoWithIcon itemInfo) { @@ -483,7 +504,13 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mLastOriginalText = label; mLastModifiedText = mLastOriginalText; mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER); - setText(label); + if (Flags.useNewIconForArchivedApps() + && info instanceof ItemInfoWithIcon infoWithIcon + && infoWithIcon.isInactiveArchive()) { + setTextWithArchivingIcon(label); + } else { + setText(label); + } } if (info.contentDescription != null) { setContentDescription(info.isDisabled() @@ -793,7 +820,13 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, getLineSpacingExtra()); if (!TextUtils.equals(modifiedString, mLastModifiedText)) { mLastModifiedText = modifiedString; - setText(modifiedString); + if (Flags.useNewIconForArchivedApps() + && getTag() instanceof ItemInfoWithIcon infoWithIcon + && infoWithIcon.isInactiveArchive()) { + setTextWithArchivingIcon(modifiedString); + } else { + setText(modifiedString); + } // if text contains NEW_LINE, set max lines to 2 if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) { setSingleLine(false); @@ -814,6 +847,44 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, super.setTextColor(getModifiedColor()); } + /** + * Sets text with a start icon for App Archiving. + * Uses a bolded drawable if text is bolded. + * @param text + */ + private void setTextWithArchivingIcon(CharSequence text) { + var drawableId = R.drawable.cloud_download_24px; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S + && getResources().getConfiguration().fontWeightAdjustment >= BOLD_TEXT_ADJUSTMENT) { + // If System bold text setting is on, then use a bolded icon + drawableId = R.drawable.cloud_download_semibold_24px; + } + setTextWithStartIcon(text, drawableId); + } + + /** + * Uses a SpannableString to set text with a Drawable at the start of the TextView + * @param text text to use for TextView + * @param drawableId Drawable Resource to use for drawing image at start of text + */ + @VisibleForTesting + public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) { + Drawable drawable = getContext().getDrawable(drawableId); + if (drawable == null) { + setText(text); + Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources" + + ", will just set text instead."); + return; + } + drawable.setTint(getCurrentTextColor()); + drawable.setBounds(0, 0, Math.round(getTextSize()), Math.round(getTextSize())); + ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER); + // First space will be replaced with Drawable, second space is for space before text. + SpannableString spannable = new SpannableString(" " + text); + spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + setText(spannable); + } + @Override public void setTextColor(ColorStateList colors) { mTextColor = colors.getDefaultColor(); @@ -972,12 +1043,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, /** Applies the given progress level to the this icon's progress bar. */ @Nullable public PreloadIconDrawable applyProgressLevel() { - if (!(getTag() instanceof ItemInfoWithIcon) + if (!(getTag() instanceof ItemInfoWithIcon info) || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) { return null; } - ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); int progressLevel = info.getProgressLevel(); if (progressLevel >= 100) { setContentDescription(info.contentDescription != null @@ -997,6 +1067,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } else { preloadIconDrawable = makePreloadIcon(); setIcon(preloadIconDrawable); + if (info.isArchived() && Flags.useNewIconForArchivedApps()) { + // reapply text without cloud icon as soon as unarchiving is triggered + applyLabel(info); + } } return preloadIconDrawable; } @@ -1291,13 +1365,4 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, public boolean canShowLongPressPopup() { return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag()); } - - /** Returns the package name of the app this icon represents. */ - public String getTargetPackageName() { - Object tag = getTag(); - if (tag instanceof ItemInfo itemInfo) { - return itemInfo.getTargetPackage(); - } - return null; - } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 7e9e8645e6..ee72c222eb 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -86,7 +86,7 @@ import java.util.Stack; public class CellLayout extends ViewGroup { private static final String TAG = "CellLayout"; - private static final boolean LOGD = false; + private static final boolean LOGD = true; /** The color of the "leave-behind" shape when a folder is opened from Hotseat. */ private static final int FOLDER_LEAVE_BEHIND_COLOR = Color.argb(160, 245, 245, 245); @@ -166,6 +166,7 @@ public class CellLayout extends ViewGroup { private final int[] mDragCellSpan = new int[2]; private boolean mDragging = false; + public boolean mHasOnLayoutBeenCalled = false; private final TimeInterpolator mEaseOutInterpolator; protected final ShortcutAndWidgetContainer mShortcutsAndWidgets; @@ -1009,6 +1010,7 @@ public class CellLayout extends ViewGroup { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { + mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done? int left = getPaddingLeft(); left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f); int right = r - l - getPaddingRight(); diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 00db3a3886..483f5f84d2 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -25,7 +25,6 @@ import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_LANDS import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT; import static com.android.launcher3.Utilities.dpiFromPx; import static com.android.launcher3.Utilities.pxFromSp; -import static com.android.launcher3.config.FeatureFlags.ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.icons.GraphicsUtils.getShapePath; import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; @@ -65,7 +64,6 @@ import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecT import com.android.launcher3.responsive.ResponsiveSpec.DimensionType; import com.android.launcher3.responsive.ResponsiveSpecsProvider; import com.android.launcher3.util.CellContentDimensions; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.IconSizeSteps; import com.android.launcher3.util.ResourceHelper; @@ -298,9 +296,6 @@ public class DeviceProfile { // the widgetView, such that the actual view size is same as the widget size. public final Rect widgetPadding = new Rect(); - // When true, nav bar is on the left side of the screen. - private boolean mIsSeascape; - // Notification dots public final DotRenderer mDotRendererWorkSpace; public final DotRenderer mDotRendererAllApps; @@ -320,6 +315,74 @@ public class DeviceProfile { // DragController public int flingToDeleteThresholdVelocity; + /** Used only as an alternative to mocking when null values cannot be used. */ + @VisibleForTesting + public DeviceProfile() { + inv = null; + mInfo = null; + mMetrics = null; + mIconSizeSteps = null; + isTablet = false; + isPhone = false; + transposeLayoutWithOrientation = false; + isMultiDisplay = false; + isTwoPanels = false; + isPredictiveBackSwipe = false; + isQsbInline = false; + isLandscape = false; + isMultiWindowMode = false; + isGestureMode = false; + isLeftRightSplit = false; + windowX = 0; + windowY = 0; + widthPx = 0; + heightPx = 0; + availableWidthPx = 0; + availableHeightPx = 0; + rotationHint = 0; + aspectRatio = 1; + mIsScalableGrid = false; + mTypeIndex = 0; + mIsResponsiveGrid = false; + desiredWorkspaceHorizontalMarginOriginalPx = 0; + edgeMarginPx = 0; + workspaceContentScale = 0; + workspaceSpringLoadedMinNextPageVisiblePx = 0; + extraSpace = 0; + workspacePageIndicatorHeight = 0; + mWorkspacePageIndicatorOverlapWorkspace = 0; + numFolderRows = 0; + numFolderColumns = 0; + folderLabelTextScale = 0; + areNavButtonsInline = false; + mHotseatBarEdgePaddingPx = 0; + mHotseatBarWorkspaceSpacePx = 0; + hotseatQsbWidth = 0; + hotseatQsbHeight = 0; + hotseatQsbVisualHeight = 0; + hotseatQsbShadowHeight = 0; + hotseatBorderSpace = 0; + mMinHotseatIconSpacePx = 0; + mMinHotseatQsbWidthPx = 0; + mMaxHotseatIconSpacePx = 0; + inlineNavButtonsEndSpacingPx = 0; + mBubbleBarSpaceThresholdPx = 0; + numShownAllAppsColumns = 0; + overviewActionsHeight = 0; + overviewActionsTopMarginPx = 0; + overviewActionsButtonSpacing = 0; + mViewScaleProvider = null; + mDotRendererWorkSpace = null; + mDotRendererAllApps = null; + taskbarHeight = 0; + stashedTaskbarHeight = 0; + taskbarBottomMargin = 0; + taskbarIconSize = 0; + mTransientTaskbarClaimedSpace = 0; + startAlignTaskbar = false; + isTransientTaskbar = false; + } + /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */ DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds, SparseArray dotRendererCache, boolean isMultiWindowMode, @@ -439,7 +502,7 @@ public class DeviceProfile { bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration); if (isTablet) { bottomSheetWorkspaceScale = workspaceContentScale; - if (isMultiDisplay && !ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH.get()) { + if (isMultiDisplay) { // TODO(b/259893832): Revert to use maxWallpaperScale to calculate bottomSheetDepth // when screen recorder bug is fixed. if (enableScalingRevealHomeAnimation()) { @@ -2008,25 +2071,8 @@ public class DeviceProfile { return isLandscape && transposeLayoutWithOrientation; } - /** - * Updates orientation information and returns true if it has changed from the previous value. - */ - public boolean updateIsSeascape(Context context) { - if (isVerticalBarLayout()) { - boolean isSeascape = DisplayController.INSTANCE.get(context) - .getInfo().rotation == Surface.ROTATION_270; - if (mIsSeascape != isSeascape) { - mIsSeascape = isSeascape; - // Hotseat changing sides requires updating workspace left/right paddings - updateWorkspacePadding(); - return true; - } - } - return false; - } - public boolean isSeascape() { - return isVerticalBarLayout() && mIsSeascape; + return rotationHint == Surface.ROTATION_270 && isVerticalBarLayout(); } public boolean shouldFadeAdjacentWorkspaceScreens() { diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt index e022159d15..f1029b1f2b 100644 --- a/src/com/android/launcher3/DropTargetHandler.kt +++ b/src/com/android/launcher3/DropTargetHandler.kt @@ -35,8 +35,7 @@ class DropTargetHandler(launcher: Launcher) { target?.let { deferred.mPackageName = it.packageName mLauncher.addEventCallback(EVENT_RESUMED) { deferred.onLauncherResume() } - } - ?: deferred.sendFailure() + } ?: deferred.sendFailure() } } } @@ -47,19 +46,10 @@ class DropTargetHandler(launcher: Launcher) { mLauncher.appWidgetHolder.startConfigActivity( mLauncher, widgetId, - ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET + ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET, ) } - fun dismissPrediction( - announcement: CharSequence, - onActionClicked: Runnable, - onDismiss: Runnable? - ) { - mLauncher.dragLayer.announceForAccessibility(announcement) - Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, onDismiss, onActionClicked) - } - fun getViewUnderDrag(info: ItemInfo): View? { return if ( info is LauncherAppWidgetInfo && @@ -95,7 +85,7 @@ class DropTargetHandler(launcher: Launcher) { R.string.item_removed, R.string.undo, mLauncher.modelWriter::commitDelete, - onUndoClicked + onUndoClicked, ) } diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java index eff748a2b0..17084bb531 100644 --- a/src/com/android/launcher3/FastScrollRecyclerView.java +++ b/src/com/android/launcher3/FastScrollRecyclerView.java @@ -25,9 +25,9 @@ import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; -import com.android.app.animation.Interpolators; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.views.RecyclerViewFastScroller; @@ -55,9 +55,12 @@ public abstract class FastScrollRecyclerView extends RecyclerView { super(context, attrs, defStyleAttr); } - public void bindFastScrollbar(RecyclerViewFastScroller scrollbar) { + public void bindFastScrollbar(RecyclerViewFastScroller scrollbar, + RecyclerViewFastScroller.FastScrollerLocation location) { mScrollbar = scrollbar; mScrollbar.setRecyclerView(this); + mScrollbar.setFastScrollerLocation(location); + scrollToTop(); onUpdateScrollbar(0); } @@ -163,6 +166,13 @@ public abstract class FastScrollRecyclerView extends RecyclerView { */ public abstract void onUpdateScrollbar(int dy); + /** + * Return the fast scroll letter list view in the A-Z list. + */ + public ConstraintLayout getLetterList() { + return null; + } + /** *

Override in each subclass of this base class. */ diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index 854645418a..024dde477f 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -33,15 +33,34 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.IntDef; + import com.android.launcher3.util.HorizontalInsettableView; +import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; +import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.views.ActivityContext; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * View class that represents the bottom row of the home screen. */ public class Hotseat extends CellLayout implements Insettable { + public static final int ALPHA_CHANNEL_TASKBAR_ALIGNMENT = 0; + public static final int ALPHA_CHANNEL_PREVIEW_RENDERER = 1; + public static final int ALPHA_CHANNEL_TASKBAR_STASH = 2; + public static final int ALPHA_CHANNEL_CHANNELS_COUNT = 3; + + @Retention(RetentionPolicy.RUNTIME) + @IntDef({ALPHA_CHANNEL_TASKBAR_ALIGNMENT, ALPHA_CHANNEL_PREVIEW_RENDERER, + ALPHA_CHANNEL_TASKBAR_STASH}) + public @interface HotseatQsbAlphaId { + } + // Ratio of empty space, qsb should take up to appear visually centered. public static final float QSB_CENTER_FACTOR = .325f; private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250; @@ -50,6 +69,8 @@ public class Hotseat extends CellLayout implements Insettable { private boolean mHasVerticalHotseat; private Workspace mWorkspace; private boolean mSendTouchToWorkspace; + private final MultiValueAlpha mIconsAlphaChannels; + private final MultiValueAlpha mQsbAlphaChannels; private final View mQsb; @@ -63,9 +84,11 @@ public class Hotseat extends CellLayout implements Insettable { public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); addView(mQsb); + mIconsAlphaChannels = new MultiValueAlpha(getShortcutsAndWidgets(), + ALPHA_CHANNEL_CHANNELS_COUNT); + mQsbAlphaChannels = new MultiValueAlpha(mQsb, ALPHA_CHANNEL_CHANNELS_COUNT); } /** @@ -97,10 +120,9 @@ public class Hotseat extends CellLayout implements Insettable { if (bubbleBarEnabled) { float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext()); if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) { - getShortcutsAndWidgets().setTranslationProvider(child -> { - int index = getShortcutsAndWidgets().indexOfChild(child); + getShortcutsAndWidgets().setTranslationProvider(cellX -> { float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace; - return dp.iconSizePx + index * borderSpaceDelta; + return dp.iconSizePx + cellX * borderSpaceDelta; }); if (mQsb instanceof HorizontalInsettableView) { HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; @@ -147,10 +169,7 @@ public class Hotseat extends CellLayout implements Insettable { // update the translation provider for future layout passes of hotseat icons. if (isBubbleBarVisible) { - icons.setTranslationProvider(child -> { - int index = icons.indexOfChild(child); - return dp.iconSizePx + index * borderSpaceDelta; - }); + icons.setTranslationProvider(cellX -> dp.iconSizePx + cellX * borderSpaceDelta); } else { icons.setTranslationProvider(null); } @@ -166,14 +185,14 @@ public class Hotseat extends CellLayout implements Insettable { animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); } } - if (mQsb instanceof HorizontalInsettableView) { - HorizontalInsettableView horizontalInsettableQsb = (HorizontalInsettableView) mQsb; - ValueAnimator qsbAnimator = ValueAnimator.ofFloat(0f, 1f); + if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) { + final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets(); + final float targetInsetFraction = + isBubbleBarVisible ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0; + ValueAnimator qsbAnimator = + ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction); qsbAnimator.addUpdateListener(animation -> { - float fraction = qsbAnimator.getAnimatedFraction(); - float insetFraction = isBubbleBarVisible - ? (float) dp.iconSizePx * fraction / dp.hotseatQsbWidth - : (float) dp.iconSizePx * (1 - fraction) / dp.hotseatQsbWidth; + float insetFraction = (float) animation.getAnimatedValue(); horizontalInsettableQsb.setHorizontalInsets(insetFraction); }); animatorSet.play(qsbAnimator); @@ -274,21 +293,27 @@ public class Hotseat extends CellLayout implements Insettable { } /** - * Sets the alpha value of just our ShortcutAndWidgetContainer. + * Sets the alpha value of the specified alpha channel of just our ShortcutAndWidgetContainer. */ - public void setIconsAlpha(float alpha) { - getShortcutsAndWidgets().setAlpha(alpha); + public void setIconsAlpha(float alpha, @HotseatQsbAlphaId int channelId) { + getIconsAlpha(channelId).setValue(alpha); } /** * Sets the alpha value of just our QSB. */ - public void setQsbAlpha(float alpha) { - mQsb.setAlpha(alpha); + public void setQsbAlpha(float alpha, @HotseatQsbAlphaId int channelId) { + getQsbAlpha(channelId).setValue(alpha); } - public float getIconsAlpha() { - return getShortcutsAndWidgets().getAlpha(); + /** Returns the alpha channel for ShortcutAndWidgetContainer */ + public MultiProperty getIconsAlpha(@HotseatQsbAlphaId int channelId) { + return mIconsAlphaChannels.get(channelId); + } + + /** Returns the alpha channel for Qsb */ + public MultiProperty getQsbAlpha(@HotseatQsbAlphaId int channelId) { + return mQsbAlphaChannels.get(channelId); } /** @@ -298,4 +323,24 @@ public class Hotseat extends CellLayout implements Insettable { return mQsb; } + /** Dumps the Hotseat internal state */ + public void dump(String prefix, PrintWriter writer) { + writer.println(prefix + "Hotseat:"); + mIconsAlphaChannels.dump( + prefix + "\t", + writer, + "mIconsAlphaChannels", + "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", + "ALPHA_CHANNEL_PREVIEW_RENDERER", + "ALPHA_CHANNEL_TASKBAR_STASH"); + mQsbAlphaChannels.dump( + prefix + "\t", + writer, + "mQsbAlphaChannels", + "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", + "ALPHA_CHANNEL_PREVIEW_RENDERER", + "ALPHA_CHANNEL_TASKBAR_STASH" + ); + } + } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 4e566abddc..b0ec9b0573 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -70,12 +70,12 @@ import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE; import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE; +import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE; import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW; import static com.android.launcher3.logging.StatsLogManager.EventEnum; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME; @@ -134,14 +134,12 @@ import android.os.Trace; import android.os.UserHandle; import android.text.TextUtils; import android.text.method.TextKeyListener; -import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MotionEvent; import android.view.View; @@ -166,7 +164,6 @@ import androidx.annotation.VisibleForTesting; import androidx.core.os.BuildCompat; import androidx.window.embedding.RuleController; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.allapps.ActivityAllAppsContainerView; @@ -181,13 +178,14 @@ import com.android.launcher3.celllayout.CellPosMapper.CellPos; import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.debug.TestEvent; +import com.android.launcher3.debug.TestEventEmitter; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.dragndrop.LauncherDragController; import com.android.launcher3.folder.Folder; -import com.android.launcher3.folder.FolderGridOrganizer; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.icons.IconCache; import com.android.launcher3.keyboard.ViewGroupFocusHelper; @@ -213,7 +211,6 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationListener; -import com.android.launcher3.pageindicators.WorkspacePageIndicator; import com.android.launcher3.pm.PinRequestHelper; import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.popup.PopupDataProvider; @@ -243,6 +240,7 @@ import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.ScreenOnTracker; import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener; import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.StableViewInfo; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.Themes; import com.android.launcher3.util.Thunk; @@ -265,6 +263,7 @@ import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.custom.CustomWidgetManager; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; import com.android.launcher3.widget.util.WidgetSizes; import com.android.systemui.plugins.LauncherOverlayPlugin; import com.android.systemui.plugins.PluginListener; @@ -370,6 +369,7 @@ public class Launcher extends StatefulActivity private LauncherAccessibilityDelegate mAccessibilityDelegate; private PopupDataProvider mPopupDataProvider; + private WidgetPickerDataProvider mWidgetPickerDataProvider; // We only want to get the SharedPreferences once since it does an FS stat each time we get // it from the context. @@ -435,6 +435,10 @@ public class Launcher extends StatefulActivity mIsColdStartupAfterReboot = sIsNewProcess && !LockedUserState.get(this).isUserUnlockedAtLauncherStartup(); if (mIsColdStartupAfterReboot) { + /* + * This trace is used to calculate the time from create to the point that icons are + * visible. + */ Trace.beginAsyncSection( COLD_STARTUP_TRACE_METHOD_NAME, COLD_STARTUP_TRACE_COOKIE); } @@ -444,12 +448,10 @@ public class Launcher extends StatefulActivity .logStart(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION) .logStart(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE); // Only use a hard-coded cookie since we only want to trace this once. - if (Utilities.ATLEAST_S) { - Trace.beginAsyncSection( - DISPLAY_WORKSPACE_TRACE_METHOD_NAME, DISPLAY_WORKSPACE_TRACE_COOKIE); - Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME, - DISPLAY_ALL_APPS_TRACE_COOKIE); - } + Trace.beginAsyncSection( + DISPLAY_WORKSPACE_TRACE_METHOD_NAME, DISPLAY_WORKSPACE_TRACE_COOKIE); + Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME, + DISPLAY_ALL_APPS_TRACE_COOKIE); TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT); if (DEBUG_STRICT_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() @@ -531,6 +533,7 @@ public class Launcher extends StatefulActivity mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace)); mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots); + mWidgetPickerDataProvider = new WidgetPickerDataProvider(); boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this); if (internalStateHandled) { @@ -595,6 +598,7 @@ public class Launcher extends StatefulActivity RuleController.getInstance(this).setRules( RuleController.parseRules(this, R.xml.split_configuration)); } + TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE); } protected ModelCallbacks createModelCallbacks() { @@ -726,13 +730,6 @@ public class Launcher extends StatefulActivity public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE); - // Starting with Android S, onEnterAnimationComplete is sent immediately - // causing the surface to get removed before the animation completed (b/175345344). - // Instead we rely on next user touch event to remove the view and optionally a callback - // from system from Android T onwards. - if (!Utilities.ATLEAST_S) { - AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE); - } } @Override @@ -771,6 +768,7 @@ public class Launcher extends StatefulActivity // initialized properly. onSaveInstanceState(new Bundle()); mModel.rebindCallbacks(); + updateDisallowBack(); } finally { Trace.endSection(); } @@ -813,7 +811,7 @@ public class Launcher extends StatefulActivity View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container); if (collectionIcon instanceof FolderIcon folderIcon && collectionIcon.getTag() instanceof FolderInfo) { - if (new FolderGridOrganizer(getDeviceProfile()) + if (createFolderGridOrganizer(getDeviceProfile()) .setFolderInfo((FolderInfo) folderIcon.getTag()) .isItemInPreview(info.rank)) { folderIcon.invalidate(); @@ -1243,9 +1241,7 @@ public class Launcher extends StatefulActivity * Returns {@link EventEnum} that should be logged when Launcher enters into AllApps state. */ protected Optional getAllAppsEntryEvent() { - return Optional.of(FeatureFlags.ENABLE_DEVICE_SEARCH.get() - ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH - : LAUNCHER_ALLAPPS_ENTRY); + return Optional.of(LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH); } @Override @@ -1404,15 +1400,6 @@ public class Launcher extends StatefulActivity this, R.attr.isWorkspaceDarkText) ? Color.BLACK : Color.WHITE); } - @Override - public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { - if (WorkspacePageIndicator.class.getName().equals(name)) { - return LayoutInflater.from(context).inflate(R.layout.page_indicator_dots, - (ViewGroup) parent, false); - } - return super.onCreateView(parent, name, context, attrs); - } - /** * Add a shortcut to the workspace or to a Folder. * @@ -2390,10 +2377,6 @@ public class Launcher extends StatefulActivity .logEnd(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION) .log() .reset(); - if (mIsColdStartupAfterReboot) { - Trace.endAsyncSection(COLD_STARTUP_TRACE_METHOD_NAME, - COLD_STARTUP_TRACE_COOKIE); - } }); } @@ -2402,6 +2385,10 @@ public class Launcher extends StatefulActivity RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) { mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal, workspaceItemCount, isBindSync); + if (mIsColdStartupAfterReboot) { + Trace.endAsyncSection(COLD_STARTUP_TRACE_METHOD_NAME, + COLD_STARTUP_TRACE_COOKIE); + } } /** @@ -2426,17 +2413,16 @@ public class Launcher extends StatefulActivity * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close * animation. * - * @param preferredItemId The id of the preferred item to match to if it exists, - * or ItemInfo#NO_MATCHING_ID if you want to not match by item id + * @param svi The StableViewInfo of the preferred item to match to if it exists or null * @param packageName The package name of the app to match. * @param user The user of the app to match. * @param supportsAllAppsState If true and we are in All Apps state, looks for view in All Apps. * Else we only looks on the workspace. */ - public @Nullable View getFirstMatchForAppClose(int preferredItemId, String packageName, + public @Nullable View getFirstMatchForAppClose( + @Nullable StableViewInfo svi, String packageName, UserHandle user, boolean supportsAllAppsState) { - final Predicate preferredItem = info -> - info != null && info.id == preferredItemId; + final Predicate preferredItem = svi == null ? i -> false : svi::matches; final Predicate packageAndUserAndApp = info -> info != null && info.itemType == ITEM_TYPE_APPLICATION @@ -2527,6 +2513,9 @@ public class Launcher extends StatefulActivity final int itemCount = container.getChildCount(); for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { View item = container.getChildAt(itemIdx); + if (item.getVisibility() != View.VISIBLE) { + continue; + } if (item instanceof ViewGroup viewGroup) { View view = mapOverViewGroup(viewGroup, op); if (view != null) { @@ -2581,10 +2570,8 @@ public class Launcher extends StatefulActivity public void bindAllApplications(AppInfo[] apps, int flags, Map packageUserKeytoUidMap) { mModelCallbacks.bindAllApplications(apps, flags, packageUserKeytoUidMap); - if (Utilities.ATLEAST_S) { - Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME, - DISPLAY_ALL_APPS_TRACE_COOKIE); - } + Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME, + DISPLAY_ALL_APPS_TRACE_COOKIE); } /** @@ -2677,6 +2664,7 @@ public class Launcher extends StatefulActivity } writer.println(prefix + " Hotseat"); + mHotseat.dump(prefix, writer); ViewGroup layout = mHotseat.getShortcutsAndWidgets(); for (int j = 0; j < layout.getChildCount(); j++) { Object tag = layout.getChildAt(j).getTag(); @@ -2695,12 +2683,31 @@ public class Launcher extends StatefulActivity writer.println(prefix + "\tmAppWidgetHolder.isListening: " + mAppWidgetHolder.isListening()); + // b/349929393 + // The only way to reproduce this bug is to ensure that onLayout never gets called. This + // theoretically is impossible, so these logs are being added to test if that actually is + // what is happening. + writer.println(prefix + "\tmWorkspace.mHasOnLayoutBeenCalled=" + + mWorkspace.mHasOnLayoutBeenCalled); + for (int i = 0; i < mWorkspace.getPageCount(); i++) { + CellLayout cellLayout = (CellLayout) mWorkspace.getPageAt(i); + writer.println(prefix + "\tcellLayout." + i + ".mHasOnLayoutBeenCalled=" + + cellLayout.mHasOnLayoutBeenCalled); + writer.println(prefix + "\tshortcutAndWidgetContainer." + i + ".mHasOnLayoutBeenCalled=" + + cellLayout.getShortcutsAndWidgets().mHasOnLayoutBeenCalled); + } + // Extra logging for general debugging mDragLayer.dump(prefix, writer); mStateManager.dump(prefix, writer); mPopupDataProvider.dump(prefix, writer); + mWidgetPickerDataProvider.dump(prefix, writer); mDeviceProfile.dump(this, prefix, writer); mAppsView.getAppsStore().dump(prefix, writer); + mAppsView.getPersonalAppList().dump(prefix, writer); + if (mAppsView.shouldShowTabs()) { + mAppsView.getWorkAppList().dump(prefix, writer); + } try { FileLog.flushAll(writer); @@ -2797,9 +2804,11 @@ public class Launcher extends StatefulActivity } private void updateDisallowBack() { - if (BuildCompat.isAtLeastV() && Flags.enableDesktopWindowingMode() - && mDeviceProfile.isTablet) { - // TODO(b/330183377) disable back in launcher when when we productionize + if (BuildCompat.isAtLeastV() + && Flags.enableDesktopWindowingMode() + && !Flags.enableDesktopWindowingWallpaperActivity() + && mDeviceProfile.isTablet) { + // TODO(b/333533253): Clean up after desktop wallpaper activity flag is rolled out return; } LauncherRootView rv = getRootView(); @@ -3006,11 +3015,18 @@ public class Launcher extends StatefulActivity return mPopupDataProvider; } + @NonNull + @Override + public WidgetPickerDataProvider getWidgetPickerDataProvider() { + return mWidgetPickerDataProvider; + } + @Override public DotInfo getDotInfoForItem(ItemInfo info) { return mPopupDataProvider.getDotInfoForItem(info); } + @NonNull public LauncherOverlayManager getOverlayManager() { return mOverlayManager; } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 3b8ff62d8a..15641ab516 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -24,6 +24,7 @@ import static com.android.launcher3.LauncherPrefs.ICON_STATE; import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI; @@ -63,6 +64,9 @@ import com.android.launcher3.util.Themes; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.widget.custom.CustomWidgetManager; +import java.util.Locale; +import java.util.Objects; + public class LauncherAppState implements SafeCloseable { public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher"; @@ -111,18 +115,30 @@ public class LauncherAppState implements SafeCloseable { if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) { ArchiveCompatibilityParams params = new ArchiveCompatibilityParams(); params.setEnableUnarchivalConfirmation(false); + params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps()); launcherApps.setArchiveCompatibility(params); } SimpleBroadcastReceiver modelChangeReceiver = - new SimpleBroadcastReceiver(mModel::onBroadcastIntent); - modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED, + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, mModel::onBroadcastIntent); + final Locale oldLocale = mContext.getResources().getConfiguration().locale; + modelChangeReceiver.register( + mContext, + () -> { + // if local has changed before receiver is registered on bg thread, + // mModel needs to reload. + Locale newLocale = mContext.getResources().getConfiguration().locale; + if (!Objects.equals(oldLocale, newLocale)) { + mModel.forceReload(); + } + }, + Intent.ACTION_LOCALE_CHANGED, ACTION_DEVICE_POLICY_RESOURCE_UPDATED); if (BuildConfig.IS_STUDIO_BUILD) { mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD), RECEIVER_EXPORTED); } - mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver)); + mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely(mContext)); SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext) .addUserEventListener(mModel::onUserEvent); diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java index 40873be369..4c82e5676f 100644 --- a/src/com/android/launcher3/LauncherApplication.java +++ b/src/com/android/launcher3/LauncherApplication.java @@ -17,14 +17,32 @@ package com.android.launcher3; import android.app.Application; +import com.android.launcher3.dagger.DaggerLauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherBaseAppComponent; + /** * Main application class for Launcher */ public class LauncherApplication extends Application { + private LauncherBaseAppComponent mAppComponent; @Override public void onCreate() { super.onCreate(); MainProcessInitializer.initialize(this); + initDagger(); + } + + public LauncherAppComponent getAppComponent() { + // Since supertype setters will return a supertype.builder and @Component.Builder types + // must not have any generic types. + // We need to cast mAppComponent to {@link LauncherAppComponent} since appContext() + // method is defined in the super class LauncherBaseComponent#Builder. + return (LauncherAppComponent) mAppComponent; + } + + protected void initDagger() { + mAppComponent = DaggerLauncherAppComponent.builder().appContext(this).build(); } } diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt index 13062b6b1b..496d517e9a 100644 --- a/src/com/android/launcher3/ModelCallbacks.kt +++ b/src/com/android/launcher3/ModelCallbacks.kt @@ -11,6 +11,8 @@ import com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID import com.android.launcher3.allapps.AllAppsStore import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.debug.TestEvent +import com.android.launcher3.debug.TestEventEmitter import com.android.launcher3.model.BgDataModel import com.android.launcher3.model.StringCache import com.android.launcher3.model.data.AppInfo @@ -59,7 +61,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { AbstractFloatingView.closeOpenViews( launcher, true, - AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(), ) workspaceLoading = true @@ -74,7 +76,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { TAG, "startBinding: " + "hotseat layout was vertical: ${launcher.hotseat?.isHasVerticalHotseat}" + - " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}" + " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}", ) launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout) TraceHelper.INSTANCE.endSection() @@ -86,14 +88,12 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { pendingTasks: RunnableList, onCompleteSignal: RunnableList, workspaceItemCount: Int, - isBindSync: Boolean + isBindSync: Boolean, ) { - if (Utilities.ATLEAST_S) { - Trace.endAsyncSection( - TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME, - TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE - ) - } + Trace.endAsyncSection( + TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME, + TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE, + ) synchronouslyBoundPages = boundPages pagesToBindSynchronously = LIntSet() clearPendingBinds() @@ -147,15 +147,16 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { // Cache one page worth of icons launcher.viewCache.setCacheSize( R.layout.folder_application, - deviceProfile.numFolderColumns * deviceProfile.numFolderRows + deviceProfile.numFolderColumns * deviceProfile.numFolderRows, ) launcher.viewCache.setCacheSize(R.layout.folder_page, 2) TraceHelper.INSTANCE.endSection() launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true) launcher.workspace.pageIndicator.setPauseScroll( /*pause=*/ false, - deviceProfile.isTwoPanels + deviceProfile.isTwoPanels, ) + TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING) } /** @@ -179,7 +180,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { val snackbar = AbstractFloatingView.getOpenView( launcher, - AbstractFloatingView.TYPE_SNACKBAR + AbstractFloatingView.TYPE_SNACKBAR, ) snackbar?.post { snackbar.close(true) } } @@ -188,7 +189,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { override fun bindAllApplications( apps: Array?, flags: Int, - packageUserKeytoUidMap: Map? + packageUserKeytoUidMap: Map?, ) { Preconditions.assertUIThread() val hadWorkApps = launcher.appsView.shouldShowTabs() @@ -251,8 +252,8 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { PopupContainerWithArrow.dismissInvalidPopup(launcher) } - override fun bindAllWidgets(allWidgets: List?) { - launcher.popupDataProvider.allWidgets = allWidgets + override fun bindAllWidgets(allWidgets: List) { + launcher.widgetPickerDataProvider.setWidgets(allWidgets, /* defaultWidgets= */ listOf()) } /** Returns the ids of the workspaces to bind. */ @@ -301,14 +302,15 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { } val widgetsListBaseEntry: WidgetsListBaseEntry = - launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry -> + launcher.widgetPickerDataProvider.get().allWidgets.firstOrNull { + item: WidgetsListBaseEntry -> item.mPkgItem.packageName == BuildConfig.APPLICATION_ID } ?: return val info = PendingAddWidgetInfo( widgetsListBaseEntry.mWidgets[0].widgetInfo, - LauncherSettings.Favorites.CONTAINER_DESKTOP + LauncherSettings.Favorites.CONTAINER_DESKTOP, ) launcher.addPendingItem( info, @@ -316,14 +318,14 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { WorkspaceLayoutManager.FIRST_SCREEN_ID, intArrayOf(0, 0), info.spanX, - info.spanY + info.spanY, ) } override fun bindScreens(orderedScreenIds: LIntArray) { launcher.workspace.pageIndicator.setPauseScroll( /*pause=*/ true, - launcher.deviceProfile.isTwoPanels + launcher.deviceProfile.isTwoPanels, ) val firstScreenPosition = 0 if ( @@ -350,7 +352,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { override fun bindAppsAdded( newScreens: LIntArray?, addNotAnimated: java.util.ArrayList?, - addAnimated: java.util.ArrayList? + addAnimated: java.util.ArrayList?, ) { // Add the new screens if (newScreens != null) { diff --git a/src/com/android/launcher3/MotionEventsUtils.java b/src/com/android/launcher3/MotionEventsUtils.java index 3228ec6942..fb244b07f3 100644 --- a/src/com/android/launcher3/MotionEventsUtils.java +++ b/src/com/android/launcher3/MotionEventsUtils.java @@ -18,8 +18,6 @@ package com.android.launcher3; import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; -import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE; - import android.annotation.TargetApi; import android.os.Build; import android.view.MotionEvent; @@ -35,14 +33,12 @@ public class MotionEventsUtils { @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static boolean isTrackpadScroll(MotionEvent event) { - return ENABLE_TRACKPAD_GESTURE.get() - && event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE; + return event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE; } @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static boolean isTrackpadMultiFingerSwipe(MotionEvent event) { - return ENABLE_TRACKPAD_GESTURE.get() - && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE; + return event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE; } public static boolean isTrackpadThreeFingerSwipe(MotionEvent event) { diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 365fbd3919..0ec3b79071 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -1463,6 +1463,15 @@ public abstract class PagedView extends ViewGrou mEdgeGlowLeft.onFlingVelocity(velocity); mEdgeGlowRight.onFlingVelocity(velocity); } + + // Detect if user tries to swipe to -1 page but gets disallowed by checking if there was + // left-over values in mEdgeGlowLeft (or mEdgeGlowRight in RLT). + final int layoutDir = getLayoutDirection(); + if ((mEdgeGlowLeft.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_LTR) + || (mEdgeGlowRight.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_RTL)) { + onDisallowSwipeToMinusOnePage(); + } + mEdgeGlowLeft.onRelease(ev); mEdgeGlowRight.onRelease(ev); // End any intermediate reordering states @@ -1487,6 +1496,8 @@ public abstract class PagedView extends ViewGrou return true; } + protected void onDisallowSwipeToMinusOnePage() {} + protected void onNotSnappingToPageInFreeScroll() { } /** diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index 0a4fb73a86..8d1e61f9ba 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -7,7 +7,6 @@ import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate. import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.INVALID; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED; @@ -36,7 +35,6 @@ import android.widget.Toast; import androidx.annotation.Nullable; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.logging.FileLog; import com.android.launcher3.logging.InstanceId; @@ -242,8 +240,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList @Override public void completeDrop(final DragObject d) { - ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo, - d.logInstanceId); + ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo); mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d); } @@ -275,7 +272,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList * Performs the drop action and returns the target component for the dragObject or null if * the action was not performed. */ - protected ComponentName performDropAction(View view, ItemInfo info, InstanceId instanceId) { + protected ComponentName performDropAction(View view, ItemInfo info) { if (mCurrentAccessibilityAction == RECONFIGURE) { int widgetId = getReconfigurableWidgetId(view); if (widgetId != INVALID_APPWIDGET_ID) { @@ -283,21 +280,6 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList } return null; } - if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { - if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) { - CharSequence announcement = getContext().getString(R.string.item_removed); - mDropTargetHandler - .dismissPrediction(announcement, () -> { - }, () -> { - mStatsLogManager.logger() - .withInstanceId(instanceId) - .withItemInfo(info) - .log(LAUNCHER_DISMISS_PREDICTION_UNDO); - }); - } - return null; - } - return performUninstall(getContext(), getUninstallTarget(getContext(), info), info); } @@ -332,9 +314,8 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList @Override public void onAccessibilityDrop(View view, ItemInfo item) { - InstanceId instanceId = new InstanceIdSequence().newInstanceId(); - doLog(instanceId, item); - performDropAction(view, item, instanceId); + doLog(new InstanceIdSequence().newInstanceId(), item); + performDropAction(view, item); } /** diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index 7484b64829..a8733f2e69 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -64,6 +64,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. private final ActivityContext mActivity; private boolean mInvertIfRtl = false; + public boolean mHasOnLayoutBeenCalled = false; @Nullable private TranslationProvider mTranslationProvider = null; @@ -201,6 +202,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { Trace.beginSection("ShortcutAndWidgetConteiner#onLayout"); + mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done? int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); @@ -245,7 +247,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. } child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); if (mTranslationProvider != null) { - final float tx = mTranslationProvider.getTranslationX(child); + final float tx = mTranslationProvider.getTranslationX(lp.getCellX()); if (child instanceof Reorderable) { ((Reorderable) child).getTranslateDelegate() .getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM) @@ -330,6 +332,6 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. /** Provides translation values to apply when laying out child views. */ interface TranslationProvider { - float getTranslationX(View child); + float getTranslationX(int cellX); } } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 19a3002665..f8ac48a7df 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -106,8 +106,6 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Various utilities shared amongst the Launcher's classes. @@ -116,8 +114,7 @@ public final class Utilities { private static final String TAG = "Launcher.Utilities"; - private static final Pattern sTrimPattern = - Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + private static final String TRIM_PATTERN = "(^\\h+|\\h+$)"; private static final Matrix sMatrix = new Matrix(); private static final Matrix sInverseMatrix = new Matrix(); @@ -125,9 +122,6 @@ public final class Utilities { public static final String[] EMPTY_STRING_ARRAY = new String[0]; public static final Person[] EMPTY_PERSON_ARRAY = new Person[0]; - @ChecksSdkIntAtLeast(api = VERSION_CODES.S) - public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; - @ChecksSdkIntAtLeast(api = VERSION_CODES.TIRAMISU, codename = "T") public static final boolean ATLEAST_T = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; @@ -181,6 +175,11 @@ public final class Utilities { sIsRunningInTestHarness = true; } + /** Disables running test in test harness mode */ + public static void disableRunningInTestHarnessForTests() { + sIsRunningInTestHarness = false; + } + public static boolean isPropertyEnabled(String propertyName) { return Log.isLoggable(propertyName, Log.VERBOSE); } @@ -380,6 +379,28 @@ public final class Utilities { outPivot.y = dst.top + dst.height() * pivotYPct; } + /** + * Scales a {@code RectF} in place about a specified pivot point. + * + *

This method modifies the given {@code RectF} directly to scale it proportionally + * by the given {@code scale}, while preserving its center at the specified + * {@code (pivotX, pivotY)} coordinates. + * + * @param rectF the {@code RectF} to scale, modified directly. + * @param pivotX the x-coordinate of the pivot point about which to scale. + * @param pivotY the y-coordinate of the pivot point about which to scale. + * @param scale the factor by which to scale the rectangle. Values less than 1 will + * shrink the rectangle, while values greater than 1 will enlarge it. + */ + public static void scaleRectFAboutPivot(RectF rectF, float pivotX, float pivotY, float scale) { + rectF.offset(-pivotX, -pivotY); + rectF.left *= scale; + rectF.top *= scale; + rectF.right *= scale; + rectF.bottom *= scale; + rectF.offset(pivotX, pivotY); + } + /** * Maps t from one range to another range. * @param t The value to map. @@ -423,10 +444,7 @@ public final class Utilities { if (s == null) { return ""; } - - // Just strip any sequence of whitespace or java space characters from the beginning and end - Matcher m = sTrimPattern.matcher(s); - return m.replaceAll("$1"); + return s.toString().replaceAll(TRIM_PATTERN, "").trim(); } /** @@ -700,9 +718,54 @@ public final class Utilities { } /** - * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent + * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CW. Parent * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine * the final bounds. + * + * As an example if this is the input: + * +-------------+ + * | +-----+ | + * | | | | + * | +-----+ | + * | | + * | | + * | | + * +-------------+ + * This would be case delta % 4 == 0: + * +-------------+ + * | +-----+ | + * | | | | + * | +-----+ | + * | | + * | | + * | | + * +-------------+ + * This would be case delta % 4 == 1: + * +----------------+ + * | +--+ | + * | | | | + * | | | | + * | +--+ | + * | | + * +----------------+ + * This would be case delta % 4 == 2: // This is case was reverted to previous behaviour which + * doesn't match the illustration due to b/353965234 + * +-------------+ + * | | + * | | + * | | + * | +-----+ | + * | | | | + * | +-----+ | + * +-------------+ + * This would be case delta % 4 == 3: + * +----------------+ + * | +--+ | + * | | | | + * | | | | + * | +--+ | + * | | + * +----------------+ */ public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, int delta) { @@ -808,6 +871,9 @@ public final class Utilities { @NonNull Rect inclusionBounds, @NonNull Rect exclusionBounds, @AdjustmentDirection int adjustmentDirection) { + if (!Rect.intersects(targetViewBounds, exclusionBounds)) { + return; + } switch (adjustmentDirection) { case TRANSLATE_RIGHT: targetView.setTranslationX(Math.min( diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index e601a3e298..0e9c8610e6 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -80,6 +80,8 @@ import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.celllayout.CellPosMapper; import com.android.launcher3.celllayout.CellPosMapper.CellPos; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.debug.TestEvent; +import com.android.launcher3.debug.TestEventEmitter; import com.android.launcher3.dot.FolderDotInfo; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; @@ -233,6 +235,7 @@ public class Workspace extends PagedView boolean mChildrenLayersEnabled = true; private boolean mStripScreensOnPageStopMoving = false; + public boolean mHasOnLayoutBeenCalled = false; private boolean mWorkspaceFadeInAdjacentScreens; @@ -314,7 +317,6 @@ public class Workspace extends PagedView */ public Workspace(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mLauncher = Launcher.getLauncher(context); mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this); mWallpaperManager = WallpaperManager.getInstance(context); @@ -1121,6 +1123,11 @@ public class Workspace extends PagedView return super.onTouchEvent(ev); } + @Override + protected void onDisallowSwipeToMinusOnePage() { + mLauncher.getOverlayManager().onDisallowSwipeToMinusOnePage(); + } + /** * Called directly from a CellLayout (not by the framework), after we've been added as a * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout @@ -1444,6 +1451,7 @@ public class Workspace extends PagedView @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done? if (mUnlockWallpaperFromDefaultPageOnLayout) { mWallpaperOffset.setLockToDefaultPage(false); mUnlockWallpaperFromDefaultPageOnLayout = false; @@ -2218,6 +2226,7 @@ public class Workspace extends PagedView if (d.stateAnnouncer != null && !droppedOnOriginalCell) { d.stateAnnouncer.completeAction(R.string.item_moved); } + TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP); } @Nullable diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java deleted file mode 100644 index 79b81871cc..0000000000 --- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility; - -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.OnHierarchyChangeListener; - -import androidx.annotation.Nullable; - -import com.android.launcher3.CellLayout; -import com.android.launcher3.DropTarget.DragObject; -import com.android.launcher3.Launcher; -import com.android.launcher3.dragndrop.DragController.DragListener; -import com.android.launcher3.dragndrop.DragOptions; - -import java.util.function.Function; - -/** - * Utility listener to enable/disable accessibility drag flags for a ViewGroup - * containing CellLayouts - */ -public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener { - - private final ViewGroup mViewGroup; - private final Function mDelegateFactory; - - /** - * @param parent the viewgroup containing all the children - * @param delegateFactory function to create no delegates - */ - public AccessibleDragListenerAdapter(ViewGroup parent, - Function delegateFactory) { - mViewGroup = parent; - mDelegateFactory = delegateFactory; - } - - @Override - public void onDragStart(DragObject dragObject, DragOptions options) { - mViewGroup.setOnHierarchyChangeListener(this); - enableAccessibleDrag(true, dragObject); - } - - @Override - public void onDragEnd() { - mViewGroup.setOnHierarchyChangeListener(null); - enableAccessibleDrag(false, null); - Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this); - } - - - @Override - public void onChildViewAdded(View parent, View child) { - if (parent == mViewGroup) { - setEnableForLayout((CellLayout) child, true); - } - } - - @Override - public void onChildViewRemoved(View parent, View child) { - if (parent == mViewGroup) { - setEnableForLayout((CellLayout) child, false); - } - } - - protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) { - for (int i = 0; i < mViewGroup.getChildCount(); i++) { - setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable); - } - } - - protected final void setEnableForLayout(CellLayout layout, boolean enable) { - layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null); - } -} diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt new file mode 100644 index 0000000000..21c2cafa3c --- /dev/null +++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility + +import android.view.View +import android.view.ViewGroup +import com.android.launcher3.CellLayout +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.dragndrop.DragController +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.views.ActivityContext +import java.util.function.Function + +/** + * Utility listener to enable/disable accessibility drag flags for a ViewGroup containing + * CellLayouts + */ +open class AccessibleDragListenerAdapter +/** + * @param parent the viewgroup containing all the children + * @param delegateFactory function to create no delegates + */ +( + private val mViewGroup: ViewGroup, + private val mDelegateFactory: Function +) : DragController.DragListener, ViewGroup.OnHierarchyChangeListener { + override fun onDragStart(dragObject: DragObject, options: DragOptions) { + mViewGroup.setOnHierarchyChangeListener(this) + enableAccessibleDrag(true, dragObject) + } + + override fun onDragEnd() { + mViewGroup.setOnHierarchyChangeListener(null) + enableAccessibleDrag(false, null) + val activityContext = ActivityContext.lookupContext(mViewGroup.context) as ActivityContext + activityContext.getDragController>()?.removeDragListener(this) + } + + override fun onChildViewAdded(parent: View, child: View) { + if (parent === mViewGroup) { + setEnableForLayout(child as CellLayout, true) + } + } + + override fun onChildViewRemoved(parent: View, child: View) { + if (parent === mViewGroup) { + setEnableForLayout(child as CellLayout, false) + } + } + + protected open fun enableAccessibleDrag(enable: Boolean, dragObject: DragObject?) { + for (i in 0 until mViewGroup.childCount) { + setEnableForLayout(mViewGroup.getChildAt(i) as CellLayout, enable) + } + } + + protected fun setEnableForLayout(layout: CellLayout, enable: Boolean) { + layout.setDragAndDropAccessibilityDelegate( + if (enable) mDelegateFactory.apply(layout) else null + ) + } +} diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java index d0fc17534e..6f73e07d62 100644 --- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java @@ -29,9 +29,9 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.customview.widget.ExploreByTouchHelper; import com.android.launcher3.CellLayout; -import com.android.launcher3.Launcher; import com.android.launcher3.R; -import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BaseDragLayer; import java.util.List; @@ -47,16 +47,17 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel protected final CellLayout mView; protected final Context mContext; + protected final ActivityContext mActivityContext; protected final LauncherAccessibilityDelegate mDelegate; - protected final DragLayer mDragLayer; + protected final BaseDragLayer mDragLayer; public DragAndDropAccessibilityDelegate(CellLayout forView) { super(forView); mView = forView; mContext = mView.getContext(); - Launcher launcher = Launcher.getLauncher(mContext); - mDelegate = launcher.getAccessibilityDelegate(); - mDragLayer = launcher.getDragLayer(); + mActivityContext = ActivityContext.lookupContext(mContext); + mDelegate = (LauncherAccessibilityDelegate) mActivityContext.getAccessibilityDelegate(); + mDragLayer = mActivityContext.getDragLayer(); } @Override diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 56a7fef526..10947683bb 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -18,16 +18,16 @@ package com.android.launcher3.allapps; import static com.android.launcher3.Flags.enableExpandingPauseWorkButton; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.WORK; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; -import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; +import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -64,6 +64,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.VisibleForTesting; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.graphics.ColorUtils; import androidx.recyclerview.widget.RecyclerView; @@ -71,6 +72,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.Flags; import com.android.launcher3.Insettable; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.R; @@ -166,6 +168,7 @@ public class ActivityAllAppsContainerView protected FloatingHeaderView mHeader; protected View mBottomSheetBackground; protected RecyclerViewFastScroller mFastScroller; + private ConstraintLayout mFastScrollLetterLayout; /** * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. @@ -280,6 +283,13 @@ public class ActivityAllAppsContainerView mSearchRecyclerView = findViewById(R.id.search_results_list_view); mFastScroller = findViewById(R.id.fast_scroller); mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); + mFastScrollLetterLayout = findViewById(R.id.scroll_letter_layout); + if (Flags.letterFastScroller()) { + // Set clip children to false otherwise the scroller letters will be clipped. + setClipChildren(false); + } else { + setClipChildren(true); + } mSearchContainer = inflateSearchBar(); if (!isSearchBarFloating()) { @@ -561,7 +571,8 @@ public class ActivityAllAppsContainerView mActivityContext.hideKeyboard(); } if (mAH.get(currentActivePage).mRecyclerView != null) { - mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller); + mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller, + ALL_APPS_SCROLLER); } // Header keeps track of active recycler view to properly render header protection. mHeader.setActiveRV(currentActivePage); @@ -666,18 +677,13 @@ public class ActivityAllAppsContainerView @NonNull AllAppsRecyclerView mainRecyclerView, @Nullable AllAppsRecyclerView workRecyclerView, @NonNull AllAppsRecyclerViewPool recycledViewPool) { - if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) { - return; - } final boolean hasWorkProfile = workRecyclerView != null; recycledViewPool.setHasWorkProfile(hasWorkProfile); mainRecyclerView.setRecycledViewPool(recycledViewPool); if (workRecyclerView != null) { workRecyclerView.setRecycledViewPool(recycledViewPool); } - if (ALL_APPS_GONE_VISIBILITY.get()) { - mainRecyclerView.updatePoolSize(hasWorkProfile); - } + mainRecyclerView.updatePoolSize(hasWorkProfile); } private void replaceAppsRVContainer(boolean showTabs) { @@ -722,9 +728,7 @@ public class ActivityAllAppsContainerView removeCustomRules(rvContainer); removeCustomRules(getSearchRecyclerView()); - if (!isSearchSupported()) { - layoutWithoutSearchContainer(rvContainer, showTabs); - } else if (isSearchBarFloating()) { + if (isSearchBarFloating()) { alignParentTop(rvContainer, showTabs); alignParentTop(getSearchRecyclerView(), /* tabs= */ false); } else { @@ -755,15 +759,23 @@ public class ActivityAllAppsContainerView }); removeCustomRules(mHeader); - if (!isSearchSupported()) { - layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */); - } else if (isSearchBarFloating()) { + if (isSearchBarFloating()) { alignParentTop(mHeader, false /* includeTabsMargin */); } else { layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); } } + /** + * Force header height update with an offset. Used by {@link UniversalSearchInputView} to + * request {@link FloatingHeaderView} to update its maxTranslation for multiline search bar. + */ + public void forceUpdateHeaderHeight(int offset) { + if (Flags.multilineSearchBar()) { + mHeader.updateSearchBarOffset(offset); + } + } + protected void updateHeaderScroll(int scrolledOffset) { float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); int headerColor = getHeaderColor(prog1); @@ -902,23 +914,6 @@ public class ActivityAllAppsContainerView mMainAdapterProvider); } - // TODO(b/216683257): Remove when Taskbar All Apps supports search. - protected boolean isSearchSupported() { - return true; - } - - private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) { - if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { - return; - } - - RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); - layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); - layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin - ? R.dimen.all_apps_header_pill_height - : R.dimen.all_apps_header_top_margin); - } - public boolean isInAllApps() { // TODO: Make this abstract return true; @@ -1302,6 +1297,10 @@ public class ActivityAllAppsContainerView return mAH.get(MAIN).mAppsList; } + public AlphabeticalAppsList getWorkAppList() { + return mAH.get(WORK).mAppsList; + } + public FloatingHeaderView getFloatingHeaderView() { return mHeader; } @@ -1484,6 +1483,10 @@ public class ActivityAllAppsContainerView } } + ConstraintLayout getFastScrollerLetterList() { + return mFastScrollLetterLayout; + } + /** * redraws header protection */ @@ -1551,7 +1554,7 @@ public class ActivityAllAppsContainerView void setup(@NonNull View rv, @Nullable Predicate matcher) { mAppsList.updateItemFilter(matcher); mRecyclerView = (AllAppsRecyclerView) rv; - mRecyclerView.bindFastScrollbar(mFastScroller); + mRecyclerView.bindFastScrollbar(mFastScroller, ALL_APPS_SCROLLER); mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); mRecyclerView.setApps(mAppsList); mRecyclerView.setLayoutManager(mLayoutManager); diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index 911612ff19..77a0fe331d 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.allapps; +import static android.view.HapticFeedbackConstants.CLOCK_TICK; + import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView.ViewHolder; @@ -71,6 +73,7 @@ public class AllAppsFastScrollHelper { @Override protected int getVerticalSnapPreference() { + mRv.performHapticFeedback(CLOCK_TICK); return SNAP_TO_ANY; } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 2a472227b9..4e1e95011b 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,8 +15,9 @@ */ package com.android.launcher3.allapps; -import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; +import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT; +import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; + import static com.android.launcher3.logger.LauncherAtom.ContainerInfo; import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN; @@ -36,22 +37,29 @@ import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.FastScrollRecyclerView; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.views.ActivityContext; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -66,6 +74,7 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { protected final int mNumAppsPerRow; private final AllAppsFastScrollHelper mFastScrollHelper; private int mCumulativeVerticalScroll; + private ConstraintLayout mLetterList; protected AlphabeticalAppsList mApps; @@ -113,13 +122,11 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { // all apps. int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount() * grid.numShownAllAppsColumns; - if (ALL_APPS_GONE_VISIBILITY.get() && ENABLE_ALL_APPS_RV_PREINFLATION.get()) { - // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to - // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows + - // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly. - maxPoolSizeForAppIcons += - PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT; - } + // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to + // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows + + // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly. + maxPoolSizeForAppIcons += + PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT; if (hasWorkProfile) { maxPoolSizeForAppIcons *= 2; } @@ -238,6 +245,9 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { return; } + if (Flags.letterFastScroller() && !mScrollbar.isDraggingThumb()) { + setLettersToScrollLayout(mApps.getFastScrollerSections()); + } // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); int availableScrollHeight = getAvailableScrollHeight(); @@ -319,6 +329,80 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { return false; } + public void setLettersToScrollLayout( + List fastScrollSections) { + if (mLetterList != null) { + mLetterList.removeAllViews(); + } + Context context = getContext(); + ActivityAllAppsContainerView allAppsContainerView = + ActivityContext.lookupContext(context).getAppsView(); + mLetterList = allAppsContainerView.getFastScrollerLetterList(); + mLetterList.setPadding(0, getScrollBarTop(), 0, getScrollBarMarginBottom()); + List textViews = new ArrayList<>(); + for (int i = 0; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo sectionInfo = fastScrollSections.get(i); + LetterListTextView textView = + (LetterListTextView) LayoutInflater.from(context).inflate( + R.layout.fast_scroller_letter_list_text_view, mLetterList, false); + int viewId = View.generateViewId(); + textView.setId(viewId); + sectionInfo.setId(viewId); + textView.setText(sectionInfo.sectionName); + if (i == fastScrollSections.size() - 1) { + // The last section info is just a duplicate so that user can scroll to the bottom. + textView.setVisibility(INVISIBLE); + } + ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( + MATCH_CONSTRAINT, WRAP_CONTENT); + lp.dimensionRatio = "v,1:1"; + textView.setLayoutParams(lp); + textViews.add(textView); + mLetterList.addView(textView); + } + // Need to add an extra textview to be aligned. + LetterListTextView lastLetterListTextView = new LetterListTextView(context); + int currentId = View.generateViewId(); + lastLetterListTextView.setId(currentId); + lastLetterListTextView.setVisibility(INVISIBLE); + textViews.add(lastLetterListTextView); + mLetterList.addView(lastLetterListTextView); + constraintTextViewsVertically(mLetterList, textViews); + mLetterList.setVisibility(VISIBLE); + } + + private void constraintTextViewsVertically(ConstraintLayout constraintLayout, + List textViews) { + ConstraintSet chain = new ConstraintSet(); + chain.clone(constraintLayout); + for (int i = 0; i < textViews.size(); i++) { + LetterListTextView currentView = textViews.get(i); + if (i == 0) { + chain.connect(currentView.getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, + ConstraintSet.TOP); + } else { + chain.connect(currentView.getId(), ConstraintSet.TOP, textViews.get(i-1).getId(), + ConstraintSet.BOTTOM); + } + chain.connect(currentView.getId(), ConstraintSet.START, constraintLayout.getId(), + ConstraintSet.START); + chain.connect(currentView.getId(), ConstraintSet.END, constraintLayout.getId(), + ConstraintSet.END); + } + int[] viewIds = textViews.stream().mapToInt(TextView::getId).toArray(); + float[] weights = new float[textViews.size()]; + Arrays.fill(weights,1); // fill with 1 for equal weights + chain.createVerticalChain(constraintLayout.getId(), ConstraintSet.TOP, + constraintLayout.getId(), ConstraintSet.BOTTOM, viewIds, weights, + ConstraintSet.CHAIN_SPREAD); + chain.applyTo(constraintLayout); + } + + @Override + public ConstraintLayout getLetterList() { + return mLetterList; + } + private void logCumulativeVerticalScroll() { ActivityContext context = ActivityContext.lookupContext(getContext()); StatsLogManager mgr = context.getStatsLogManager(); diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java index 9623709348..29b9e7761d 100644 --- a/src/com/android/launcher3/allapps/AllAppsStore.java +++ b/src/com/android/launcher3/allapps/AllAppsStore.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.allapps; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK; @@ -42,6 +41,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -108,7 +108,7 @@ public class AllAppsStore { mPackageUserKeytoUidMap = map; // Preinflate all apps RV when apps has changed, which can happen after unlocking screen, // rotating screen, or downloading/upgrading apps. - if (shouldPreinflate && ENABLE_ALL_APPS_RV_PREINFLATION.get()) { + if (shouldPreinflate) { mAllAppsRecyclerViewPool.preInflateAllAppsViewHolders(mContext); } } @@ -260,8 +260,13 @@ public class AllAppsStore { public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length); for (int i = 0; i < mApps.length; i++) { - writer.println(String.format("%s\tPackage index and name: %d/%s", prefix, i, - mApps[i].componentName.getPackageName())); + writer.println(String.format(Locale.getDefault(), + "%s\tPackage index, name, class, and description: %d/%s:%s, %s", + prefix, + i, + mApps[i].componentName.getPackageName(), + mApps[i].componentName.getClassName(), + mApps[i].contentDescription)); } } } diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 1b0ad04e89..c6852e015c 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -37,7 +37,6 @@ import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; import android.animation.Animator; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.util.FloatProperty; import android.view.HapticFeedbackConstants; import android.view.View; @@ -52,11 +51,9 @@ import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.AllAppsSwipeController; @@ -359,22 +356,6 @@ public class AllAppsTransitionController }); } - if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled() - && Utilities.ATLEAST_S) { - if (toState == ALL_APPS) { - builder.addOnFrameListener( - new VibrationAnimatorUpdateListener(this, mVibratorWrapper, - SWIPE_DRAG_COMMIT_THRESHOLD, 1)); - } else { - builder.addOnFrameListener( - new VibrationAnimatorUpdateListener(this, mVibratorWrapper, - 0, SWIPE_DRAG_COMMIT_THRESHOLD)); - } - builder.addEndListener((unused) -> { - mVibratorWrapper.cancelVibrate(); - }); - } - float targetProgress = toState.getVerticalProgress(mLauncher); if (Float.compare(mProgress, targetProgress) == 0) { setAlphas(toState, config, builder); @@ -391,8 +372,7 @@ public class AllAppsTransitionController setAlphas(toState, config, builder); // This controls both haptics for tapping on QSB and going to all apps. - if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && - !FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get()) { + if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) { mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } @@ -432,8 +412,7 @@ public class AllAppsTransitionController mAppsView = appsView; mAppsView.setScrimView(scrimView); - mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT, - FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE); + mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT, View.GONE); mAppsViewAlpha.setUpdateVisibility(true); mAppsViewTranslationY = new MultiPropertyFactory<>( mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum); @@ -445,45 +424,4 @@ public class AllAppsTransitionController public void setShiftRange(float shiftRange) { mShiftRange = shiftRange; } - - /** - * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start - * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller - * as it gives an accurate progress that dictates where the vibrator should vibrate. - * Note: once the user begins a gesture and does the commit haptic, there should not be anymore - * haptics played for that gesture. - */ - private static class VibrationAnimatorUpdateListener implements - ValueAnimator.AnimatorUpdateListener { - private final VibratorWrapper mVibratorWrapper; - private final AllAppsTransitionController mController; - private final float mStartThreshold; - private final float mEndThreshold; - private boolean mHasCommitted; - - VibrationAnimatorUpdateListener(AllAppsTransitionController controller, - VibratorWrapper vibratorWrapper, float startThreshold, - float endThreshold) { - mController = controller; - mVibratorWrapper = vibratorWrapper; - mStartThreshold = startThreshold; - mEndThreshold = endThreshold; - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - if (mHasCommitted) { - return; - } - float currentProgress = - AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController); - if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) { - mVibratorWrapper.vibrateForDragTexture(); - } else if (!(currentProgress == 0 || currentProgress == 1)) { - // This check guards against committing at the location of the start of the gesture - mVibratorWrapper.vibrateForDragCommit(); - mHasCommitted = true; - } - } - } } diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 5d03a93254..8e44d65d50 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.allapps; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; @@ -25,6 +26,7 @@ import android.content.Context; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ImageSpan; +import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -38,6 +40,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.LabelComparator; import com.android.launcher3.views.ActivityContext; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -71,11 +74,17 @@ public class AlphabeticalAppsList implement public final CharSequence sectionName; // The item position public final int position; + // The view id associated with this section + public int id = -1; public FastScrollSectionInfo(CharSequence sectionName, int position) { this.sectionName = sectionName; this.position = position; } + + public void setId(int id) { + this.id = id; + } } @@ -269,6 +278,7 @@ public class AlphabeticalAppsList implement List oldItems = new ArrayList<>(mAdapterItems); // Prepare to update the list of sections, filtered apps, etc. mFastScrollerSections.clear(); + Log.d(TAG, "Clearing FastScrollerSections."); mAdapterItems.clear(); mAccessibilityResultsCount = 0; @@ -289,12 +299,22 @@ public class AlphabeticalAppsList implement mFastScrollerSections.add(new FastScrollSectionInfo( mActivityContext.getResources().getString( R.string.work_profile_edu_section), 0)); + Log.d(TAG, "Adding FastScrollSection for work edu card."); } position = addAppsWithSections(mApps, position); } if (Flags.enablePrivateSpace()) { position = addPrivateSpaceItems(position); } + if (!mFastScrollerSections.isEmpty()) { + // After all the adapterItems are added, add a view to the bottom so that user can + // scroll all the way down. + mAdapterItems.add(new AdapterItem(VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO)); + mFastScrollerSections.add(new FastScrollSectionInfo( + mFastScrollerSections.get(mFastScrollerSections.size() - 1).sectionName, + position++)); + Log.d(TAG, "Adding FastScrollSection duplicate to scroll to the bottom."); + } } mAccessibilityResultsCount = (int) mAdapterItems.stream() .filter(AdapterItem::isCountedForAccessibility).count(); @@ -337,6 +357,7 @@ public class AlphabeticalAppsList implement && !mPrivateApps.isEmpty()) { // Always add PS Header if Space is present and visible. position = mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems); + Log.d(TAG, "Adding FastScrollSection for Private Space header. "); mFastScrollerSections.add(new FastScrollSectionInfo( mPrivateProfileAppScrollerBadge, position)); int privateSpaceState = mPrivateProviderManager.getCurrentState(); @@ -398,14 +419,14 @@ public class AlphabeticalAppsList implement hasPrivateApps = appList.stream(). allMatch(mPrivateProviderManager.getItemInfoMatcher()); } + Log.d(TAG, "Adding apps with sections. HasPrivateApps: " + hasPrivateApps); for (int i = 0; i < appList.size(); i++) { AppInfo info = appList.get(i); // Apply decorator to private apps. if (hasPrivateApps) { mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info, - new SectionDecorationInfo(mActivityContext.getApplicationContext(), - getRoundRegions(i, appList.size()), - true /* decorateTogether */))); + new SectionDecorationInfo(mActivityContext, + getRoundRegions(i, appList.size()), true /* decorateTogether */))); } else { mAdapterItems.add(AdapterItem.asApp(info)); } @@ -413,6 +434,8 @@ public class AlphabeticalAppsList implement String sectionName = info.sectionName; // Create a new section if the section names do not match if (!sectionName.equals(lastSectionName)) { + Log.d(TAG, "addAppsWithSections: adding sectionName: " + sectionName + + " with appInfoTitle: " + info.title); lastSectionName = sectionName; mFastScrollerSections.add(new FastScrollSectionInfo(hasPrivateApps ? mPrivateProfileAppScrollerBadge : sectionName, position)); @@ -463,6 +486,13 @@ public class AlphabeticalAppsList implement return mPrivateProviderManager; } + public void dump(String prefix, PrintWriter writer) { + writer.println(prefix + "SectionInfo[] size: " + mFastScrollerSections.size()); + for (int i = 0; i < mFastScrollerSections.size(); i++) { + writer.println("\tFastScrollSection: " + mFastScrollerSections.get(i).sectionName); + } + } + private static class MyDiffCallback extends DiffUtil.Callback { private final List mOldList; diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 98ca420eb9..60bf3eacc6 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -26,6 +26,7 @@ import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED; import android.content.Context; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -67,7 +68,8 @@ public abstract class BaseAllAppsAdapter ex public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5; public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6; public static final int VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER = 1 << 7; - public static final int NEXT_ID = 8; + public static final int VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO = 1 << 8; + public static final int NEXT_ID = 9; // Common view type masks public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; @@ -246,6 +248,8 @@ public abstract class BaseAllAppsAdapter ex case VIEW_TYPE_PRIVATE_SPACE_HEADER: return new ViewHolder(mLayoutInflater.inflate( R.layout.private_space_header, parent, false)); + case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO: + return new ViewHolder(new View(mActivityContext)); default: if (mAdapterProvider.isViewSupported(viewType)) { return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType); @@ -278,6 +282,13 @@ public abstract class BaseAllAppsAdapter ex privateProfileManager.getReadyToAnimate()) && privateProfileManager.getCurrentState() == STATE_ENABLED ? 0 : 1); + Log.d(TAG, "onBindViewHolder: " + + "isPrivateSpaceItem: " + isPrivateSpaceItem + + " isStateTransitioning: " + privateProfileManager.isStateTransitioning() + + " isScrolling: " + privateProfileManager.isScrolling() + + " readyToAnimate: " + privateProfileManager.getReadyToAnimate() + + " currentState: " + privateProfileManager.getCurrentState() + + " currentAlpha: " + icon.getAlpha()); } // Views can still be bounded before the app list is updated hence showing icons // after collapsing. @@ -316,6 +327,7 @@ public abstract class BaseAllAppsAdapter ex == STATE_DISABLED ? null : new SectionDecorationInfo(mActivityContext, ROUND_NOTHING, true /* decorateTogether */); break; + case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO: case VIEW_TYPE_ALL_APPS_DIVIDER: case VIEW_TYPE_WORK_DISABLED_CARD: // nothing to do diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java index 92c589c966..ac06ab40e0 100644 --- a/src/com/android/launcher3/allapps/FloatingHeaderView.java +++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java @@ -30,10 +30,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; +import com.android.launcher3.Flags; import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.PluginManagerWrapper; import com.android.launcher3.views.ActivityContext; import com.android.systemui.plugins.AllAppsRow; @@ -104,6 +104,8 @@ public class FloatingHeaderView extends LinearLayout implements private boolean mFloatingRowsCollapsed; // Total height of all current floating rows. Collapsed rows == 0 height. private int mFloatingRowsHeight; + // Offset of search bar. Adds to the floating view height when multi-line is supported. + private int mSearchBarOffset = 0; // This is initialized once during inflation and stays constant after that. Fixed views // cannot be added or removed dynamically. @@ -198,6 +200,14 @@ public class FloatingHeaderView extends LinearLayout implements } } + /** + * Offset floating rows height by search bar + */ + void updateSearchBarOffset(int offset) { + mSearchBarOffset = offset; + onHeightUpdated(); + } + @Override public void onPluginDisconnected(AllAppsRow plugin) { PluginHeaderRow row = mPluginRows.get(plugin); @@ -209,15 +219,12 @@ public class FloatingHeaderView extends LinearLayout implements @Override public View getFocusedChild() { - if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { - for (FloatingHeaderRow row : mAllRows) { - if (row.hasVisibleContent() && row.isVisible()) { - return row.getFocusedChild(); - } + for (FloatingHeaderRow row : mAllRows) { + if (row.hasVisibleContent() && row.isVisible()) { + return row.getFocusedChild(); } - return null; } - return super.getFocusedChild(); + return null; } void setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV, @@ -258,9 +265,18 @@ public class FloatingHeaderView extends LinearLayout implements mTabLayout.setVisibility(mTabsHidden ? GONE : visibility); } + /** Returns whether search bar has multi-line support, and is currently in multi-line state. */ + private boolean isSearchBarMultiline() { + return Flags.multilineSearchBar() && mSearchBarOffset > 0; + } + private void updateExpectedHeight() { updateFloatingRowsHeight(); mMaxTranslation = 0; + boolean shouldAddSearchBarHeight = isSearchBarMultiline() && !Flags.floatingSearchBar(); + if (shouldAddSearchBarHeight) { + mMaxTranslation += mSearchBarOffset; + } if (mFloatingRowsCollapsed) { return; } diff --git a/src/com/android/launcher3/allapps/FloatingMaskView.java b/src/com/android/launcher3/allapps/FloatingMaskView.java index 606eb0328e..cee5e18958 100644 --- a/src/com/android/launcher3/allapps/FloatingMaskView.java +++ b/src/com/android/launcher3/allapps/FloatingMaskView.java @@ -21,6 +21,7 @@ import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.VisibleForTesting; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; @@ -53,13 +54,21 @@ public class FloatingMaskView extends ConstraintLayout { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams(); - AllAppsRecyclerView allAppsContainerView = - mActivityContext.getAppsView().getActiveRecyclerView(); + setParameters((ViewGroup.MarginLayoutParams) getLayoutParams(), + mActivityContext.getAppsView().getActiveRecyclerView()); + } + + @VisibleForTesting + void setParameters(ViewGroup.MarginLayoutParams lp, AllAppsRecyclerView recyclerView) { if (lp != null) { - lp.rightMargin = allAppsContainerView.getPaddingRight(); - lp.leftMargin = allAppsContainerView.getPaddingLeft(); - mBottomBox.setMinimumHeight(allAppsContainerView.getPaddingBottom()); + lp.rightMargin = recyclerView.getPaddingRight(); + lp.leftMargin = recyclerView.getPaddingLeft(); + getBottomBox().setMinimumHeight(recyclerView.getPaddingBottom()); } } + + @VisibleForTesting + ImageView getBottomBox() { + return mBottomBox; + } } diff --git a/src/com/android/launcher3/allapps/LetterListTextView.java b/src/com/android/launcher3/allapps/LetterListTextView.java new file mode 100644 index 0000000000..9326d79f18 --- /dev/null +++ b/src/com/android/launcher3/allapps/LetterListTextView.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.launcher3.allapps; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.core.graphics.ColorUtils; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Themes; + +/** + * A TextView that is used to display the letter list in the fast scroller. + */ +public class LetterListTextView extends TextView { + private static final float ABSOLUTE_TRANSLATION_X = 30f; + private static final float ABSOLUTE_SCALE = 1.4f; + private final Drawable mLetterBackground; + private final int mLetterListTextWidthAndHeight; + private final int mTextColor; + private final int mBackgroundColor; + private final int mSelectedColor; + + public LetterListTextView(Context context) { + this(context, null, 0); + } + + public LetterListTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LetterListTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mLetterBackground = context.getDrawable(R.drawable.bg_letter_list_text); + mLetterListTextWidthAndHeight = context.getResources().getDimensionPixelSize( + R.dimen.fastscroll_list_letter_size); + mTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurface); + mBackgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceContainer); + mSelectedColor = Themes.getAttrColor(context, R.attr.materialColorOnSecondary); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + setBackground(mLetterBackground); + setTextColor(mTextColor); + setClickable(false); + setWidth(mLetterListTextWidthAndHeight); + setTextSize(mLetterListTextWidthAndHeight); + setVisibility(VISIBLE); + } + + /** + * Animates the letter list text view based on the current finger position. + * + * @param currentFingerY The Y position of where the finger is placed on the fastScroller in + * pixels. + */ + public void animateBasedOnYPosition(int currentFingerY) { + if (getBackground() == null) { + return; + } + float cutOffMin = currentFingerY - (getHeight() * 2); + float cutOffMax = currentFingerY + (getHeight() * 2); + float cutOffDistance = cutOffMax - cutOffMin; + // Update the background blend color + boolean isWithinAnimationBounds = getY() < cutOffMax && getY() > cutOffMin; + if (isWithinAnimationBounds) { + getBackground().setColorFilter(new PorterDuffColorFilter( + getBlendColorBasedOnYPosition(currentFingerY, cutOffDistance), + PorterDuff.Mode.MULTIPLY)); + } else { + getBackground().setColorFilter(new PorterDuffColorFilter( + mBackgroundColor, PorterDuff.Mode.MULTIPLY)); + } + translateBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds); + scaleBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds); + } + + private int getBlendColorBasedOnYPosition(int y, float cutOffDistance) { + float raisedCosineBlend = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI); + float blendRatio = Utilities.boundToRange(raisedCosineBlend, 0f, 1f); + return ColorUtils.blendARGB(mBackgroundColor, mSelectedColor, blendRatio); + } + + private void scaleBasedOnYPosition(int y, float cutOffDistance, + boolean isWithinAnimationBounds) { + float raisedCosineScale = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI) + * ABSOLUTE_SCALE; + if (isWithinAnimationBounds) { + raisedCosineScale = Utilities.boundToRange(raisedCosineScale, 1f, ABSOLUTE_SCALE); + setScaleX(raisedCosineScale); + setScaleY(raisedCosineScale); + } else { + setScaleX(1); + setScaleY(1); + } + } + + private void translateBasedOnYPosition(int y, float cutOffDistance, + boolean isWithinAnimationBounds) { + float raisedCosineTranslation = + (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI) + * ABSOLUTE_TRANSLATION_X; + if (isWithinAnimationBounds) { + raisedCosineTranslation = -1 * Utilities.boundToRange(raisedCosineTranslation, + 0, ABSOLUTE_TRANSLATION_X); + setTranslationX(raisedCosineTranslation); + } else { + setTranslationX(0); + } + } +} diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java index 6f021eacf7..e215cabcd7 100644 --- a/src/com/android/launcher3/allapps/PrivateProfileManager.java +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -41,13 +41,13 @@ import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_L import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.os.UserManager; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -89,14 +89,16 @@ import java.util.function.Predicate; * logic in the Personal tab. */ public class PrivateProfileManager extends UserProfileManager { - private static final int EXPAND_COLLAPSE_DURATION = 800; + + private static final String TAG = "PrivateProfileManager"; + private static final int EXPAND_COLLAPSE_DURATION = 400; private static final int SETTINGS_OPACITY_DURATION = 400; private static final int TEXT_UNLOCK_OPACITY_DURATION = 300; private static final int TEXT_LOCK_OPACITY_DURATION = 50; private static final int APP_OPACITY_DURATION = 400; private static final int MASK_VIEW_DURATION = 200; private static final int APP_OPACITY_DELAY = 400; - private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400; + private static final int PILL_TRANSITION_DELAY = 400; private static final int SETTINGS_OPACITY_DELAY = 400; private static final int LOCK_TEXT_OPACITY_DELAY = 500; private static final int MASK_VIEW_DELAY = 400; @@ -106,6 +108,8 @@ public class PrivateProfileManager extends UserProfileManager { private final Predicate mPrivateProfileMatcher; private final int mPsHeaderHeight; private final int mFloatingMaskViewCornerRadius; + private final int mLockTextMarginStart; + private final int mLockTextMarginEnd; private final RecyclerView.OnScrollListener mOnIdleScrollListener = new RecyclerView.OnScrollListener() { @Override @@ -130,6 +134,11 @@ public class PrivateProfileManager extends UserProfileManager { private Runnable mOnPSHeaderAdded; @Nullable private RelativeLayout mPSHeader; + @Nullable + private TextView mLockText; + @Nullable + private PrivateSpaceSettingsButton mPrivateSpaceSettingsButton; + @Nullable private ConstraintLayout mFloatingMaskView; private final String mLockedStateContentDesc; private final String mUnLockedStateContentDesc; @@ -152,6 +161,10 @@ public class PrivateProfileManager extends UserProfileManager { .getString(R.string.ps_container_unlock_button_content_description); mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize( R.dimen.ps_floating_mask_corner_radius); + mLockTextMarginStart = mAllApps.getContext().getResources().getDimensionPixelSize( + R.dimen.ps_lock_icon_text_margin_start_expanded); + mLockTextMarginEnd = mAllApps.getContext().getResources().getDimensionPixelSize( + R.dimen.ps_lock_icon_text_margin_end_expanded); } /** Adds Private Space Header to the layout. */ @@ -351,19 +364,12 @@ public class PrivateProfileManager extends UserProfileManager { /** Add Private Space Header view elements based upon {@link UserProfileState} */ public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) { mPSHeader = parent; + Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Binding private space."); + updateView(); if (mOnPSHeaderAdded != null) { MAIN_EXECUTOR.execute(mOnPSHeaderAdded); mOnPSHeaderAdded = null; } - // Set the transition duration for the settings and lock button to animate. - ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); - if (mReadyToAnimate) { - enableLayoutTransition(settingAndLockGroup); - } else { - // Ensure any unwanted animations to not happen. - settingAndLockGroup.setLayoutTransition(null); - } - updateView(); } /** Update the states of the views that make up the header at the state it is called in. */ @@ -371,12 +377,15 @@ public class PrivateProfileManager extends UserProfileManager { if (mPSHeader == null) { return; } + Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: " + + getCurrentState()); mPSHeader.setAlpha(1); ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button); assert lockPill != null; - TextView lockText = lockPill.findViewById(R.id.lock_text); - PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button); - assert settingsButton != null; + mLockText = lockPill.findViewById(R.id.lock_text); + assert mLockText != null; + mPrivateSpaceSettingsButton = mPSHeader.findViewById(R.id.ps_settings_button); + assert mPrivateSpaceSettingsButton != null; //Add image for private space transitioning view ImageView transitionView = mPSHeader.findViewById(R.id.ps_transition_image); assert transitionView != null; @@ -387,12 +396,19 @@ public class PrivateProfileManager extends UserProfileManager { // Remove header from accessibility target when enabled. mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - lockText.setVisibility(VISIBLE); + if (!mReadyToAnimate) { + // Don't set visibilities when animating as the animation will handle it. + mLockText.setVisibility(VISIBLE); + mLockText.setAlpha(1); + mLockText.setHorizontallyScrolling(false); + mPrivateSpaceSettingsButton.setVisibility( + isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE); + mPrivateSpaceSettingsButton.setClickable(isPrivateSpaceSettingsAvailable()); + } lockPill.setVisibility(VISIBLE); lockPill.setOnClickListener(view -> lockingAction(/* lock */ true)); lockPill.setContentDescription(mUnLockedStateContentDesc); - settingsButton.setVisibility(isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE); transitionView.setVisibility(GONE); } case STATE_DISABLED -> { @@ -402,12 +418,15 @@ public class PrivateProfileManager extends UserProfileManager { mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); mPSHeader.setContentDescription(mLockedStateContentDesc); - lockText.setVisibility(GONE); + mLockText.setVisibility(GONE); + mLockText.setAlpha(0); + mLockText.setHorizontallyScrolling(false); lockPill.setVisibility(VISIBLE); lockPill.setOnClickListener(view -> lockingAction(/* lock */ false)); lockPill.setContentDescription(mLockedStateContentDesc); - settingsButton.setVisibility(GONE); + mPrivateSpaceSettingsButton.setVisibility(GONE); + mPrivateSpaceSettingsButton.setClickable(false); transitionView.setVisibility(GONE); } case STATE_TRANSITION -> { @@ -581,6 +600,51 @@ public class PrivateProfileManager extends UserProfileManager { return alphaAnim; } + private ValueAnimator animatePillTransition(boolean isExpanding) { + if (mLockText == null) { + return new ValueAnimator().setDuration(0); + } + mLockText.measure(0,0); + int currentWidth = mLockText.getWidth(); + int fullWidth = mLockText.getMeasuredWidth(); + float from = isExpanding ? 0 : currentWidth; + float to = isExpanding ? fullWidth : 0; + ValueAnimator pillAnim = ObjectAnimator.ofFloat(from, to); + pillAnim.setStartDelay(isExpanding ? PILL_TRANSITION_DELAY : 0); + pillAnim.setDuration(EXPAND_COLLAPSE_DURATION); + pillAnim.setInterpolator(Interpolators.STANDARD); + pillAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float translation = (float) valueAnimator.getAnimatedValue(); + float translationFraction = translation / fullWidth; + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mLockText.getLayoutParams(); + layoutParams.width = (int) translation; + layoutParams.setMarginStart((int) (mLockTextMarginStart * translationFraction)); + layoutParams.setMarginEnd((int) (mLockTextMarginEnd * translationFraction)); + mLockText.setLayoutParams(layoutParams); + mLockText.requestLayout(); + } + }); + pillAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (!isExpanding) { + mLockText.setVisibility(GONE); + } + mLockText.setHorizontallyScrolling(false); + } + + @Override + public void onAnimationStart(Animator animator) { + mLockText.setHorizontallyScrolling(true); + mLockText.setVisibility(VISIBLE); + } + }); + return pillAnim; + } + /** * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an * animation. At the moment, collapsing, setting alpha changes, and animating the text is done @@ -592,33 +656,23 @@ public class PrivateProfileManager extends UserProfileManager { } if (mPSHeader == null) { mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand); - setAnimationRunning(false); + // Set animation to true, because onBind will be called after this return where we want + // the views to be updated accordingly so animation can happen. + setAnimationRunning(true); return; } attachFloatingMaskView(expand); - ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); - if (settingsAndLockGroup.getLayoutTransition() == null) { - // Set a new transition if the current ViewGroup does not already contain one as each - // transition should only happen once when applied. - enableLayoutTransition(settingsAndLockGroup); - } - settingsAndLockGroup.getLayoutTransition().setStartDelay( - LayoutTransition.CHANGING, - expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY); - PropertySetter headerSetter = new AnimatedPropertySetter(); - headerSetter.add(updateSettingsGearAlpha(expand)); - headerSetter.add(updateLockTextAlpha(expand)); - AnimatorSet animatorSet = headerSetter.buildAnim(); + AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { + Log.d(TAG, "updatePrivateStateAnimator: Private space animation expanding: " + + expand); mStatsLogManager.logger().sendToInteractionJankMonitor( expand ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN, mAllApps.getActiveRecyclerView()); - // Animate the collapsing of the text at the same time while updating lock button. - mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); setAnimationRunning(true); } @@ -636,6 +690,11 @@ public class PrivateProfileManager extends UserProfileManager { ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END, mAllApps.getActiveRecyclerView()); + Log.d(TAG, "updatePrivateStateAnimator: lockText visibility: " + + mLockText.getVisibility() + " lockTextAlpha: " + mLockText.getAlpha()); + Log.d(TAG, "updatePrivateStateAnimator: settingsCog visibility: " + + mPrivateSpaceSettingsButton.getVisibility() + + " settingsCogAlpha: " + mPrivateSpaceSettingsButton.getAlpha()); if (!expand) { mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration( mPrivateAppsSectionDecorator); @@ -648,16 +707,24 @@ public class PrivateProfileManager extends UserProfileManager { } })); if (expand) { - animatorSet.playTogether(animateAlphaOfIcons(true), + animatorSet.playTogether(updateSettingsGearAlpha(true), + updateLockTextAlpha(true), + animateAlphaOfIcons(true), + animatePillTransition(true), translateFloatingMaskView(false)); } else { + AnimatorSet parallelSet = new AnimatorSet(); + parallelSet.playTogether(updateSettingsGearAlpha(false), + updateLockTextAlpha(false), + animateAlphaOfIcons(false), + animatePillTransition(false)); if (isPrivateSpaceHidden()) { - animatorSet.playSequentially(animateAlphaOfIcons(false), + animatorSet.playSequentially(parallelSet, animateAlphaOfPrivateSpaceContainer(), animateCollapseAnimation()); } else { animatorSet.playSequentially(translateFloatingMaskView(true), - animateAlphaOfIcons(false), + parallelSet, animateCollapseAnimation()); } } @@ -688,7 +755,7 @@ public class PrivateProfileManager extends UserProfileManager { /** Fades out the private space container. */ private ValueAnimator translateFloatingMaskView(boolean animateIn) { if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) { - return new ValueAnimator(); + return new ValueAnimator().setDuration(0); } // Translate base on the height amount. Translates out on expand and in on collapse. float floatingMaskViewHeight = getFloatingMaskViewHeight(); @@ -700,38 +767,19 @@ public class PrivateProfileManager extends UserProfileManager { alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { + if (mFloatingMaskView == null) { + return; + } mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue()); } }); return alphaAnim; } - /** Animates the layout changes when the text of the button becomes visible/gone. */ - private void enableLayoutTransition(ViewGroup settingsAndLockGroup) { - LayoutTransition settingsAndLockTransition = new LayoutTransition(); - settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); - settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); - settingsAndLockTransition.setInterpolator(LayoutTransition.CHANGING, - Interpolators.STANDARD); - settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() { - @Override - public void startTransition(LayoutTransition transition, ViewGroup viewGroup, - View view, int i) { - } - @Override - public void endTransition(LayoutTransition transition, ViewGroup viewGroup, - View view, int i) { - settingsAndLockGroup.setLayoutTransition(null); - mReadyToAnimate = false; - } - }); - settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition); - } - /** Change the settings gear alpha when expanded or collapsed. */ private ValueAnimator updateSettingsGearAlpha(boolean expand) { - if (mPSHeader == null) { - return new ValueAnimator(); + if (mPrivateSpaceSettingsButton == null || !isPrivateSpaceSettingsAvailable()) { + return new ValueAnimator().setDuration(0); } float from = expand ? 0 : 1; float to = expand ? 1 : 0; @@ -742,16 +790,29 @@ public class PrivateProfileManager extends UserProfileManager { settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { - mPSHeader.findViewById(R.id.ps_settings_button) - .setAlpha((float) valueAnimator.getAnimatedValue()); + mPrivateSpaceSettingsButton.setAlpha((float) valueAnimator.getAnimatedValue()); + } + }); + settingsAlphaAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + mPrivateSpaceSettingsButton.setVisibility(VISIBLE); + mPrivateSpaceSettingsButton.setClickable(false); + } + + @Override + public void onAnimationEnd(Animator animator) { + if (expand) { + mPrivateSpaceSettingsButton.setClickable(true); + } } }); return settingsAlphaAnim; } private ValueAnimator updateLockTextAlpha(boolean expand) { - if (mPSHeader == null) { - return new ValueAnimator(); + if (mLockText == null) { + return new ValueAnimator().setDuration(0); } float from = expand ? 0 : 1; float to = expand ? 1 : 0; @@ -762,8 +823,7 @@ public class PrivateProfileManager extends UserProfileManager { alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { - mPSHeader.findViewById(R.id.lock_text).setAlpha( - (float) valueAnimator.getAnimatedValue()); + mLockText.setAlpha((float) valueAnimator.getAnimatedValue()); } }); return alphaAnim; @@ -801,8 +861,22 @@ public class PrivateProfileManager extends UserProfileManager { if (!Flags.privateSpaceAddFloatingMaskView()) { return; } + // Use getLocationOnScreen() as simply checking for mPSHeader.getBottom() is only relative + // to its parent. + int[] psHeaderLocation = new int[2]; + mPSHeader.getLocationOnScreen(psHeaderLocation); + int psHeaderBottomY = psHeaderLocation[1] + mPsHeaderHeight; + // Calculate the topY of the floatingMaskView as if it was added. + int floatingMaskViewBottomBoxTopY = + (int) (mAllApps.getBottom() - getMainRecyclerView().getPaddingBottom()); + // Don't attach if the header will be clipped by the floating mask view. + if (psHeaderBottomY > floatingMaskViewBottomBoxTopY) { + mFloatingMaskView = null; + return; + } mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate( R.layout.private_space_mask_view, mAllApps, false); + assert mFloatingMaskView != null; mAllApps.addView(mFloatingMaskView); // Translate off the screen first if its collapsing so this header view isn't visible to // user when animation starts. diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java index ac9b146f12..eaeb8bbdd3 100644 --- a/src/com/android/launcher3/allapps/SectionDecorationHandler.java +++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java @@ -25,7 +25,6 @@ import android.graphics.drawable.InsetDrawable; import android.view.View; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import com.android.launcher3.R; import com.android.launcher3.util.Themes; @@ -61,10 +60,10 @@ public class SectionDecorationHandler { mContext = context; mFillAlpha = fillAlpha; - mFocusColor = ContextCompat.getColor(context, - R.color.material_color_surface_bright); // UX recommended - mFillColor = ContextCompat.getColor(context, - R.color.material_color_surface_container_high); // UX recommended + mFocusColor = Themes.getAttrColor(context, + R.attr.materialColorSurfaceBright); // UX recommended + mFillColor = Themes.getAttrColor(context, + R.attr.materialColorSurfaceContainerHigh); // UX recommended mIsTopLeftRound = isTopLeftRound; mIsTopRightRound = isTopRightRound; diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java index ec45415afa..de3bb9efe1 100644 --- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java +++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java @@ -22,8 +22,6 @@ import android.text.TextWatcher; import android.text.style.SuggestionSpan; import android.util.Log; import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnFocusChangeListener; import android.view.inputmethod.EditorInfo; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; @@ -31,7 +29,6 @@ import android.widget.TextView.OnEditorActionListener; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.search.SearchAlgorithm; import com.android.launcher3.search.SearchCallback; import com.android.launcher3.views.ActivityContext; @@ -40,8 +37,7 @@ import com.android.launcher3.views.ActivityContext; * An interface to a search box that AllApps can command. */ public class AllAppsSearchBarController - implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener, - OnFocusChangeListener { + implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener { private static final String TAG = "AllAppsSearchBarController"; protected ActivityContext mLauncher; @@ -69,7 +65,6 @@ public class AllAppsSearchBarController mInput.addTextChangedListener(this); mInput.setOnEditorActionListener(this); mInput.setOnBackKeyListener(this); - mInput.addOnFocusChangeListener(this); mSearchAlgorithm = searchAlgorithm; } @@ -142,13 +137,6 @@ public class AllAppsSearchBarController return false; } - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (!hasFocus && !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { - mInput.hideKeyboard(); - } - } - /** * Resets the search bar state. */ @@ -157,7 +145,6 @@ public class AllAppsSearchBarController mInput.reset(); mInput.clearFocus(); mQuery = null; - mInput.removeOnFocusChangeListener(this); } /** diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index 32445ecddb..870c8769ca 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -18,10 +18,12 @@ package com.android.launcher3.apppairs; import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.FloatProperty; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -54,6 +56,26 @@ import java.util.function.Predicate; public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable { private static final String TAG = "AppPairIcon"; + // The duration of the scaling animation on hover enter/exit. + private static final int HOVER_SCALE_DURATION = 150; + // The default scale of the icon when not hovered. + private static final Float HOVER_SCALE_DEFAULT = 1f; + // The max scale of the icon when hovered. + private static final Float HOVER_SCALE_MAX = 1.1f; + // Animates the scale of the icon background on hover. + private static final FloatProperty HOVER_SCALE_PROPERTY = + new FloatProperty<>("hoverScale") { + @Override + public void setValue(AppPairIcon view, float scale) { + view.mIconGraphic.setHoverScale(scale); + } + + @Override + public Float get(AppPairIcon view) { + return view.mIconGraphic.getHoverScale(); + } + }; + // A view that holds the app pair icon graphic. private AppPairIconGraphic mIconGraphic; // A view that holds the app pair's title. @@ -250,4 +272,14 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + + @Override + public void onHoverChanged(boolean hovered) { + super.onHoverChanged(hovered); + ObjectAnimator + .ofFloat(this, HOVER_SCALE_PROPERTY, + hovered ? HOVER_SCALE_MAX : HOVER_SCALE_DEFAULT) + .setDuration(HOVER_SCALE_DURATION) + .start(); + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java index db83d91a4b..114ed2edd5 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java @@ -26,6 +26,7 @@ import android.os.Build; import androidx.annotation.NonNull; +import com.android.launcher3.Utilities; import com.android.launcher3.icons.FastBitmapDrawable; /** @@ -128,6 +129,18 @@ public class AppPairIconDrawable extends Drawable { height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); + // Scale each background from its center edge closest to the center channel. + Utilities.scaleRectFAboutPivot( + leftSide, + leftSide.left + leftSide.width(), + leftSide.top + leftSide.centerY(), + mP.getHoverScale()); + Utilities.scaleRectFAboutPivot( + rightSide, + rightSide.left, + rightSide.top + rightSide.centerY(), + mP.getHoverScale()); + drawCustomRoundedRect(canvas, leftSide, new float[]{ mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), @@ -163,6 +176,18 @@ public class AppPairIconDrawable extends Drawable { height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); + // Scale each background from its center edge closest to the center channel. + Utilities.scaleRectFAboutPivot( + topSide, + topSide.left + topSide.centerX(), + topSide.top + topSide.height(), + mP.getHoverScale()); + Utilities.scaleRectFAboutPivot( + bottomSide, + bottomSide.left + bottomSide.centerX(), + bottomSide.top, + mP.getHoverScale()); + drawCustomRoundedRect(canvas, topSide, new float[]{ mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt index 45dc013348..5b546d69de 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt @@ -64,6 +64,8 @@ class AppPairIconDrawingParams(val context: Context, container: Int) { var isLeftRightSplit: Boolean = true // The background paint color (based on container). var bgColor: Int = 0 + // The scale of the icon background while hovered. + var hoverScale: Float = 1f init { val activity: ActivityContext = ActivityContext.lookupContext(context) diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index dce97eb594..034b686828 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -139,4 +139,19 @@ constructor(context: Context, attrs: AttributeSet? = null) : super.dispatchDraw(canvas) drawable.draw(canvas) } + + /** + * Sets the scale of the icon background while hovered. + */ + fun setHoverScale(scale: Float) { + drawParams.hoverScale = scale + redraw() + } + + /** + * Gets the scale of the icon background while hovered. + */ + fun getHoverScale(): Float { + return drawParams.hoverScale + } } diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java index 4f8d53e11c..d593f80756 100644 --- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -3,6 +3,7 @@ package com.android.launcher3.compat; import android.content.Context; import android.icu.text.AlphabeticIndex; import android.os.LocaleList; +import android.util.Log; import androidx.annotation.NonNull; @@ -12,6 +13,9 @@ import java.util.Locale; public class AlphabeticIndexCompat { + // TODO(b/336947811): Set to false after root causing is done. + private static final boolean DEBUG = true; + private static final String TAG = "AlphabeticIndexCompat"; private static final String MID_DOT = "\u2219"; private final String mDefaultMiscLabel; @@ -49,6 +53,9 @@ public class AlphabeticIndexCompat { public String computeSectionName(@NonNull CharSequence cs) { String s = Utilities.trim(cs); String sectionName = mBaseIndex.getBucket(mBaseIndex.getBucketIndex(s)).getLabel(); + if (DEBUG) { + Log.d(TAG, "computeSectionName: cs: " + cs + " sectionName: " + sectionName); + } if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) { int c = s.codePointAt(0); boolean startsWithDigit = Character.isDigit(c); diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 33e6f9106f..8fe1b34077 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -19,6 +19,7 @@ package com.android.launcher3.config; import static com.android.launcher3.config.FeatureFlags.BooleanFlag.DISABLED; import static com.android.launcher3.config.FeatureFlags.BooleanFlag.ENABLED; import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; +import static com.android.wm.shell.Flags.enableTaskbarOnPhones; import android.content.res.Resources; @@ -61,18 +62,7 @@ public final class FeatureFlags { * and set a default value for the flag. This will be the default value on Debug builds. *

*/ - // TODO(Block 2): Clean up flags - public static final BooleanFlag ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH = getDebugFlag(270395073, - "ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH", DISABLED, - "Allow bottom sheet depth to be smaller than 1 for multi-display devices."); - // TODO(Block 3): Clean up flags - public static final BooleanFlag ENABLE_DISMISS_PREDICTION_UNDO = getDebugFlag(270394476, - "ENABLE_DISMISS_PREDICTION_UNDO", DISABLED, - "Show an 'Undo' snackbar when users dismiss a predicted hotseat item"); - public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag(270395171, - "CONTINUOUS_VIEW_TREE_CAPTURE", ENABLED, "Capture View tree every frame"); - public static final BooleanFlag ENABLE_WORKSPACE_LOADING_OPTIMIZATION = getDebugFlag(251502424, "ENABLE_WORKSPACE_LOADING_OPTIMIZATION", DISABLED, "load the current workspace screen visible to the user before the rest rather than " @@ -83,32 +73,7 @@ public final class FeatureFlags { "changes the timing of the loading and binding of delegate items during " + "data preparation for loading the home screen"); - // TODO(Block 4): Cleanup flags - public static final BooleanFlag ENABLE_ALL_APPS_FROM_OVERVIEW = - getDebugFlag(275132633, "ENABLE_ALL_APPS_FROM_OVERVIEW", DISABLED, - "Allow entering All Apps from Overview (e.g. long swipe up from app)"); - - public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = getReleaseFlag( - 270394468, "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", ENABLED, - "Enable option to show keyboard when going to all-apps"); - - // TODO(Block 5): Clean up flags - public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851, - "ENABLE_TWOLINE_DEVICESEARCH", DISABLED, - "Enable two line label for icons with labels on device search."); - - public static final BooleanFlag ENABLE_ICON_IN_TEXT_HEADER = getDebugFlag(270395143, - "ENABLE_ICON_IN_TEXT_HEADER", DISABLED, "Show icon in textheader"); - - public static final BooleanFlag ENABLE_PREMIUM_HAPTICS_ALL_APPS = getDebugFlag(270396358, - "ENABLE_PREMIUM_HAPTICS_ALL_APPS", DISABLED, - "Enables haptics opening/closing All apps"); - // TODO(Block 6): Clean up flags - public static final BooleanFlag ENABLE_ALL_APPS_SEARCH_IN_TASKBAR = getDebugFlag(270393900, - "ENABLE_ALL_APPS_SEARCH_IN_TASKBAR", ENABLED, - "Enables Search box in Taskbar All Apps."); - public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag(270395140, "SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED, "Enable dragging and dropping to pin apps within secondary display"); @@ -124,10 +89,6 @@ public final class FeatureFlags { public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274, "FOLDABLE_SINGLE_PAGE", DISABLED, "Use a single page for the workspace"); - public static final BooleanFlag ENABLE_PARAMETRIZE_REORDER = getDebugFlag(289420844, - "ENABLE_PARAMETRIZE_REORDER", DISABLED, - "Enables generating the reorder using a set of parameters"); - // TODO(Block 12): Clean up flags public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680, "ENABLE_MULTI_INSTANCE", DISABLED, @@ -143,7 +104,7 @@ public final class FeatureFlags { DISABLED, "Sends a notification whenever launcher encounters an uncaught exception."); public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION = - enableTaskbarNavbarUnification() && !isPhone(); + enableTaskbarNavbarUnification() && (!isPhone() || enableTaskbarOnPhones()); private static boolean isPhone() { final boolean isPhone; @@ -174,32 +135,11 @@ public final class FeatureFlags { public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag(270390012, "PROMISE_APPS_IN_ALL_APPS", DISABLED, "Add promise icon in all-apps"); - public static final BooleanFlag KEYGUARD_ANIMATION = getDebugFlag(270390904, - "KEYGUARD_ANIMATION", DISABLED, - "Enable animation for keyguard going away on wallpaper"); - - public static final BooleanFlag ENABLE_DEVICE_SEARCH = getReleaseFlag(270390907, - "ENABLE_DEVICE_SEARCH", ENABLED, "Allows on device search in all apps"); - - public static final BooleanFlag ENABLE_HIDE_HEADER = getReleaseFlag(270390930, - "ENABLE_HIDE_HEADER", ENABLED, "Hide header on keyboard before typing in all apps"); - // Aconfig migration complete for ENABLE_EXPANDING_PAUSE_WORK_BUTTON. public static final BooleanFlag ENABLE_EXPANDING_PAUSE_WORK_BUTTON = getDebugFlag(270390779, "ENABLE_EXPANDING_PAUSE_WORK_BUTTON", DISABLED, "Expand and collapse pause work button while scrolling"); - // Aconfig migration complete for ENABLE_TWOLINE_ALLAPPS. - public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937, - "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps."); - - public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693, - "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps"); - - public static final BooleanFlag FOLDER_NAME_MAJORITY_RANKING = getDebugFlag(270391638, - "FOLDER_NAME_MAJORITY_RANKING", ENABLED, - "Suggests folder names based on majority based ranking."); - public static final BooleanFlag INJECT_FALLBACK_APP_CORPUS_RESULTS = getReleaseFlag(270391706, "INJECT_FALLBACK_APP_CORPUS_RESULTS", DISABLED, "Inject fallback app corpus result when AiAi fails to return it."); @@ -224,27 +164,7 @@ public final class FeatureFlags { return ENABLE_APP_PAIRS.get() || com.android.wm.shell.Flags.enableAppPairs(); } - // TODO(Block 19): Clean up flags - public static final BooleanFlag SCROLL_TOP_TO_RESET = getReleaseFlag(270395177, - "SCROLL_TOP_TO_RESET", ENABLED, - "Bring up IME and focus on input when scroll to top if 'Always show keyboard'" - + " is enabled or in prefix state"); - - public static final BooleanFlag ENABLE_SEARCH_UNINSTALLED_APPS = getReleaseFlag(270395269, - "ENABLE_SEARCH_UNINSTALLED_APPS", ENABLED, "Search uninstalled app results."); - // TODO(Block 20): Clean up flags - public static final BooleanFlag ENABLE_SCRIM_FOR_APP_LAUNCH = getDebugFlag(270393276, - "ENABLE_SCRIM_FOR_APP_LAUNCH", DISABLED, "Enables scrim during app launch animation."); - - public static final BooleanFlag ENABLE_BACK_SWIPE_HOME_ANIMATION = getDebugFlag(270393426, - "ENABLE_BACK_SWIPE_HOME_ANIMATION", ENABLED, - "Enables home animation to icon when user swipes back."); - - public static final BooleanFlag ENABLE_DYNAMIC_TASKBAR_THRESHOLDS = getDebugFlag(294252473, - "ENABLE_DYNAMIC_TASKBAR_THRESHOLDS", ENABLED, - "Enables taskbar thresholds that scale based on screen size."); - // Aconfig migration complete for ENABLE_HOME_TRANSITION_LISTENER. public static final BooleanFlag ENABLE_HOME_TRANSITION_LISTENER = getDebugFlag(306053414, "ENABLE_HOME_TRANSITION_LISTENER", DISABLED, @@ -263,18 +183,7 @@ public final class FeatureFlags { "ENABLE_WIDGET_TRANSITION_FOR_RESIZING", DISABLED, "Enable widget transition animation when resizing the widgets"); - public static final BooleanFlag PREEMPTIVE_UNFOLD_ANIMATION_START = getDebugFlag(270397209, - "PREEMPTIVE_UNFOLD_ANIMATION_START", ENABLED, - "Enables starting the unfold animation preemptively when unfolding, without" - + "waiting for SystemUI and then merging the SystemUI progress whenever we " - + "start receiving the events"); - // TODO(Block 25): Clean up flags - public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257, - "ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED, - "Enable the redesigned gesture navigation tutorial"); - - // TODO(Block 26): Clean up flags public static final BooleanFlag ENABLE_WIDGET_HOST_IN_BACKGROUND = getDebugFlag(270394384, "ENABLE_WIDGET_HOST_IN_BACKGROUND", ENABLED, "Enable background widget updates listening for widget holder"); @@ -299,10 +208,6 @@ public final class FeatureFlags { "SEPARATE_RECENTS_ACTIVITY", DISABLED, "Uses a separate recents activity instead of using the integrated recents+Launcher UI"); - public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = getReleaseFlag(270393258, - "ENABLE_ENFORCED_ROUNDED_CORNERS", ENABLED, - "Enforce rounded corners on all App Widgets"); - public static final BooleanFlag USE_LOCAL_ICON_OVERRIDES = getDebugFlag(270394973, "USE_LOCAL_ICON_OVERRIDES", ENABLED, "Use inbuilt monochrome icons if app doesn't provide one"); @@ -316,20 +221,15 @@ public final class FeatureFlags { com.android.wm.shell.Flags.enableSplitContextual(); } - public static final BooleanFlag ENABLE_TRACKPAD_GESTURE = getDebugFlag(271010401, - "ENABLE_TRACKPAD_GESTURE", ENABLED, "Enables trackpad gesture."); - // TODO(Block 29): Clean up flags + // Aconfig migration complete for ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT. public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag(270393897, "ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT", DISABLED, "Enables displaying the all apps button in the hotseat."); - public static final BooleanFlag ENABLE_KEYBOARD_QUICK_SWITCH = getDebugFlag(270396844, - "ENABLE_KEYBOARD_QUICK_SWITCH", ENABLED, "Enables keyboard quick switching"); - - public static final BooleanFlag ENABLE_KEYBOARD_TASKBAR_TOGGLE = getDebugFlag(281726846, - "ENABLE_KEYBOARD_TASKBAR_TOGGLE", ENABLED, - "Enables keyboard taskbar stash toggling"); + public static boolean enableAllAppsButtonInHotseat() { + return ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get() || Flags.enableAllAppsButtonInHotseat(); + } // TODO(Block 30): Clean up flags public static final BooleanFlag USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES = getDebugFlag(270395010, @@ -348,14 +248,6 @@ public final class FeatureFlags { return ENABLE_RESPONSIVE_WORKSPACE.get() || Flags.enableResponsiveWorkspace(); } - // TODO(Block 33): Clean up flags - public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355, - "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED, - "Enables preinflating all apps icons to avoid scrolling jank."); - public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514, - "ALL_APPS_GONE_VISIBILITY", ENABLED, - "Set all apps container view's hidden visibility to GONE instead of INVISIBLE."); - public static BooleanFlag getDebugFlag( int bugId, String key, BooleanFlag flagState, String description) { return flagState; diff --git a/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java new file mode 100644 index 0000000000..da13546c78 --- /dev/null +++ b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java @@ -0,0 +1,47 @@ +/* + * 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.launcher3.contextualeducation; + +import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; + +import com.android.launcher3.R; +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.SafeCloseable; +import com.android.systemui.contextualeducation.GestureType; + +/** + * A class to update contextual education data. It is a no-op implementation and could be + * overridden by changing the resource value [R.string.contextual_edu_manager_class] to provide + * a real implementation. + */ +public class ContextualEduStatsManager implements ResourceBasedOverride, SafeCloseable { + public static final MainThreadInitializedObject INSTANCE = + forOverride(ContextualEduStatsManager.class, R.string.contextual_edu_manager_class); + + /** + * Updates contextual education stats when a gesture is triggered + * @param isTrackpadGesture indicates if the gesture is triggered by trackpad + * @param gestureType type of gesture triggered + */ + public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) { + } + + @Override + public void close() { + } +} diff --git a/src/com/android/launcher3/dagger/ActivityContextScope.java b/src/com/android/launcher3/dagger/ActivityContextScope.java new file mode 100644 index 0000000000..887f15cacf --- /dev/null +++ b/src/com/android/launcher3/dagger/ActivityContextScope.java @@ -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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +/** + * Scope annotation for singletons associated with Launcher activity context. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Scope +public @interface ActivityContextScope { +} diff --git a/src/com/android/launcher3/dagger/ApplicationContext.java b/src/com/android/launcher3/dagger/ApplicationContext.java new file mode 100644 index 0000000000..9a5b08b51b --- /dev/null +++ b/src/com/android/launcher3/dagger/ApplicationContext.java @@ -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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Qualifier for Launcher application context. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface ApplicationContext { +} diff --git a/src/com/android/launcher3/dagger/LauncherAppSingleton.java b/src/com/android/launcher3/dagger/LauncherAppSingleton.java new file mode 100644 index 0000000000..92c00b6d85 --- /dev/null +++ b/src/com/android/launcher3/dagger/LauncherAppSingleton.java @@ -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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +/** + * Scope annotation for singleton items within the LauncherAppComponent. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Scope +public @interface LauncherAppSingleton { +} diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java new file mode 100644 index 0000000000..0a50e8bc9e --- /dev/null +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -0,0 +1,40 @@ +/* + * 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.dagger; + +import android.content.Context; + +import com.android.launcher3.util.DaggerSingletonTracker; + +import dagger.BindsInstance; + +/** + * Launcher base component for Dagger injection. + * + * This class is not actually annotated as a Dagger component, since it is not used directly as one. + * Doing so generates unnecessary code bloat. + * + * See {@link LauncherAppComponent} for the one actually used by AOSP. + */ +public interface LauncherBaseAppComponent { + DaggerSingletonTracker getDaggerSingletonTracker(); + /** Builder for LauncherBaseAppComponent. */ + interface Builder { + @BindsInstance Builder appContext(@ApplicationContext Context context); + LauncherBaseAppComponent build(); + } +} diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt new file mode 100644 index 0000000000..52b454f2b7 --- /dev/null +++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt @@ -0,0 +1,59 @@ +/* + * 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.debug + +import android.content.Context +import android.util.Log +import com.android.launcher3.util.MainThreadInitializedObject +import com.android.launcher3.util.SafeCloseable + +/** Events fired by the launcher. */ +enum class TestEvent(val event: String) { + LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"), + WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"), + RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"), + WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"), + SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"), + SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"), +} + +/** Interface to create TestEventEmitters. */ +interface TestEventEmitter : SafeCloseable { + + companion object { + @JvmField + val INSTANCE = + MainThreadInitializedObject { _: Context? -> + TestEventsEmitterProduction() + } + } + + fun sendEvent(event: TestEvent) +} + +/** + * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on + * tests. This is just a placeholder and test should override this class. + */ +class TestEventsEmitterProduction : TestEventEmitter { + + override fun close() {} + + override fun sendEvent(event: TestEvent) { + Log.d("TestEventsEmitterProduction", "Event sent ${event.event}") + } +} diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index bc5a164da8..c50c008f30 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -27,6 +27,7 @@ import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; import com.android.launcher3.DragSource; @@ -69,8 +70,9 @@ public abstract class DragController */ protected DragDriver mDragDriver = null; + @VisibleForTesting /** Options controlling the drag behavior. */ - protected DragOptions mOptions; + public DragOptions mOptions; /** Coordinate for motion down event */ protected final Point mMotionDown = new Point(); @@ -79,7 +81,8 @@ public abstract class DragController protected final Point mTmpPoint = new Point(); - protected DropTarget.DragObject mDragObject; + @VisibleForTesting + public DropTarget.DragObject mDragObject; /** Who can receive drop events */ private final ArrayList mDropTargets = new ArrayList<>(); diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index db693f0fa9..8b1f42b91f 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -157,7 +157,8 @@ public class DragLayer extends BaseDragLayer implements LauncherOverla isOverFolderOrSearchBar = isEventOverView(topView, ev) || isEventOverAccessibleDropTargetBar(ev); if (!isOverFolderOrSearchBar) { - sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + sendTapOutsideFolderAccessibilityEvent( + currentFolder.getIsEditingName()); mHoverPointClosesFolder = true; return true; } @@ -167,7 +168,8 @@ public class DragLayer extends BaseDragLayer implements LauncherOverla isOverFolderOrSearchBar = isEventOverView(topView, ev) || isEventOverAccessibleDropTargetBar(ev); if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { - sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + sendTapOutsideFolderAccessibilityEvent( + currentFolder.getIsEditingName()); mHoverPointClosesFolder = true; return true; } else if (!isOverFolderOrSearchBar) { diff --git a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java index fbe9e33d5a..bebef70b2e 100644 --- a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java +++ b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java @@ -26,7 +26,7 @@ import com.android.launcher3.Workspace; public class SpringLoadedDragController implements OnAlarmListener { // how long the user must hover over a mini-screen before it unshrinks private static final long ENTER_SPRING_LOAD_HOVER_TIME = 500; - private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 2000; + private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 3000; private static final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950; Alarm mAlarm; diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index dcc55e61b6..7bec768b64 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -26,6 +26,7 @@ import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS; +import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED; import static com.android.launcher3.testing.shared.TestProtocol.FOLDER_OPENED_MESSAGE; @@ -65,6 +66,7 @@ import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.content.res.ResourcesCompat; import com.android.launcher3.AbstractFloatingView; @@ -134,7 +136,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo * We avoid measuring {@link #mContent} with a 0 width or height, as this * results in CellLayout being measured as UNSPECIFIED, which it does not support. */ - private static final int MIN_CONTENT_DIMEN = 5; + @VisibleForTesting + static final int MIN_CONTENT_DIMEN = 5; public static final int STATE_CLOSED = 0; public static final int STATE_ANIMATING = 1; @@ -142,7 +145,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN}) - public @interface FolderState {} + public @interface FolderState { + } /** * Time for which the scroll hint is shown before automatically changing page. @@ -163,7 +167,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo private static final int FOLDER_COLOR_ANIMATION_DURATION = 200; private static final int REORDER_DELAY = 250; - private static final int ON_EXIT_CLOSE_DELAY = 400; + static final int ON_EXIT_CLOSE_DELAY = 400; private static final Rect sTempRect = new Rect(); private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10; @@ -183,10 +187,10 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo || itemType == ITEM_TYPE_APP_PAIR; } - private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper()); - private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper()); - private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper()); - final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper()); + private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper()); + private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper()); + private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper()); + private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper()); final ArrayList mItemsInReadingOrder = new ArrayList(); @@ -196,7 +200,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar). // Anything specific to Launcher should use mLauncherDelegate, otherwise should // use mActivityContext. - protected final LauncherDelegate mLauncherDelegate; + protected LauncherDelegate mLauncherDelegate; protected final ActivityContext mActivityContext; protected DragController mDragController; @@ -209,7 +213,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Thunk FolderPagedView mContent; - public FolderNameEditText mFolderName; + private FolderNameEditText mFolderName; private PageIndicatorDots mPageIndicator; protected View mFooter; @@ -233,10 +237,10 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener; @ViewDebug.ExportedProperty(category = "launcher") private boolean mRearrangeOnClose = false; - boolean mItemsInvalidated = false; + private boolean mItemsInvalidated = false; private View mCurrentDragView; private boolean mIsExternalDrag; - private boolean mDragInProgress = false; + private boolean mIsDragInProgress = false; private boolean mDeleteFolderOnDropCompleted = false; private boolean mSuppressFolderDeletion = false; private boolean mItemAddedBackToSelfViaIcon = false; @@ -249,7 +253,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo private int mScrollAreaOffset; @Thunk - int mScrollHintDir = SCROLL_NONE; + private int mScrollHintDir = SCROLL_NONE; @Thunk int mCurrentScrollDir = SCROLL_NONE; @@ -314,9 +318,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo | InputType.TYPE_TEXT_FLAG_CAP_WORDS); mFolderName.forceDisableSuggestions(true); mFolderName.setPadding(mFolderName.getPaddingLeft(), - (mFooterHeight - mFolderName.getLineHeight()) / 2, + (getFooterHeight() - mFolderName.getLineHeight()) / 2, mFolderName.getPaddingRight(), - (mFooterHeight - mFolderName.getLineHeight()) / 2); + (getFooterHeight() - mFolderName.getLineHeight()) / 2); mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this); setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback); @@ -324,42 +328,54 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public boolean onLongClick(View v) { // Return if global dragging is not enabled - if (!mLauncherDelegate.isDraggingEnabled()) return true; + if (!getIsLauncherDraggingEnabled()) return true; return startDrag(v, new DragOptions()); } + @VisibleForTesting + boolean getIsLauncherDraggingEnabled() { + return mLauncherDelegate.isDraggingEnabled(); + } + public boolean startDrag(View v, DragOptions options) { Object tag = v.getTag(); if (tag instanceof ItemInfo item) { mEmptyCellRank = item.rank; mCurrentDragView = v; - mDragController.addDragListener(this); - if (options.isAccessibleDrag) { - mDragController.addDragListener(new AccessibleDragListenerAdapter( - mContent, FolderAccessibilityHelper::new) { - @Override - protected void enableAccessibleDrag(boolean enable, - @Nullable DragObject dragObject) { - super.enableAccessibleDrag(enable, dragObject); - mFooter.setImportantForAccessibility(enable - ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : IMPORTANT_FOR_ACCESSIBILITY_AUTO); - } - }); - } - - mLauncherDelegate.beginDragShared(v, this, options); + addDragListener(options); + callBeginDragShared(v, options); } return true; } + void callBeginDragShared(View v, DragOptions options) { + mLauncherDelegate.beginDragShared(v, this, options); + } + + void addDragListener(DragOptions options) { + getDragController().addDragListener(this); + if (!options.isAccessibleDrag) { + return; + } + getDragController().addDragListener(new AccessibleDragListenerAdapter( + mContent, FolderAccessibilityHelper::new) { + @Override + protected void enableAccessibleDrag(boolean enable, + @Nullable DragObject dragObject) { + super.enableAccessibleDrag(enable, dragObject); + mFooter.setImportantForAccessibility(enable + ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + }); + } + @Override public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { if (dragObject.dragSource != this) { return; } - mContent.removeItem(mCurrentDragView); mItemsInvalidated = true; @@ -368,29 +384,23 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo try (SuppressInfoChanges s = new SuppressInfoChanges()) { mInfo.remove(dragObject.dragInfo, true); } - mDragInProgress = true; + mIsDragInProgress = true; mItemAddedBackToSelfViaIcon = false; } @Override public void onDragEnd() { - if (mIsExternalDrag && mDragInProgress) { + if (mIsExternalDrag && mIsDragInProgress) { completeDragExit(); } - mDragInProgress = false; - mDragController.removeDragListener(this); - } - - public boolean isEditingName() { - return mIsEditingName; + mIsDragInProgress = false; + getDragController().removeDragListener(this); } public void startEditingFolderName() { - post(() -> { - showLabelSuggestions(); - mFolderName.setHint(""); - mIsEditingName = true; - }); + showLabelSuggestions(); + mFolderName.setHint(""); + mIsEditingName = true; } @Override @@ -458,7 +468,11 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return mFolderIcon; } - public void setDragController(DragController dragController) { + DragController getDragController() { + return mDragController; + } + + void setDragController(DragController dragController) { mDragController = dragController; } @@ -539,7 +553,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push * rest of the suggestions to InputMethodManager. */ - private void showLabelSuggestions() { + void showLabelSuggestions() { if (mInfo.suggestedFolderNames == null) { return; } @@ -633,11 +647,11 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo */ public void beginExternalDrag() { mIsExternalDrag = true; - mDragInProgress = true; + mIsDragInProgress = true; // Since this folder opened by another controller, it might not get onDrop or // onDropComplete. Perform cleanup once drag-n-drop ends. - mDragController.addDragListener(this); + getDragController().addDragListener(this); ArrayList items = new ArrayList<>(mInfo.getContents()); mEmptyCellRank = items.size(); @@ -661,16 +675,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo * is played. */ private void animateOpen(List items, int pageNo) { - if (items == null || items.size() <= 1) { - Log.d(TAG, "Couldn't animate folder open because items is: " + items); + if (!shouldAnimateOpen(items)) { return; } Folder openFolder = getOpen(mActivityContext); - if (openFolder != null && openFolder != this) { - // Close any open folder before opening a folder. - openFolder.close(true); - } + closeOpenFolder(openFolder); mContent.bindItems(items); centerAboutIcon(); @@ -684,7 +694,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // There was a one-off crash where the folder had a parent already. if (getParent() == null) { dragLayer.addView(this); - mDragController.addDropTarget(this); + getDragController().addDropTarget(this); } else { if (FeatureFlags.IS_STUDIO_BUILD) { Log.e(TAG, "Opening folder (" + this + ") which already has a parent:" @@ -733,7 +743,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // Do not update the flag if we are in drag mode. The flag will be updated, when we // actually drop the icon. - final boolean updateAnimationFlag = !mDragInProgress; + final boolean updateAnimationFlag = !mIsDragInProgress; anim.addListener(new AnimatorListenerAdapter() { @SuppressLint("InlinedApi") @@ -763,16 +773,41 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo addAnimationStartListeners(anim); // Because t=0 has the folder match the folder icon, we can skip the // first frame and have the same movement one frame earlier. + Log.d("b/311077782", "Folder.animateOpen"); anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration())); anim.start(); // Make sure the folder picks up the last drag move even if the finger doesn't move. - if (mDragController.isDragging()) { - mDragController.forceTouchMove(); + if (getDragController().isDragging()) { + getDragController().forceTouchMove(); } mContent.verifyVisibleHighResIcons(mContent.getNextPage()); } + /** + * Determines whether we should animate the folder opening. + */ + boolean shouldAnimateOpen(List items) { + if (items == null || items.size() <= 1) { + Log.d(TAG, "Couldn't animate folder open because items is: " + items); + return false; + } + return true; + } + + /** + * If there's a folder already open, we want to close it before opening another one. + */ + @VisibleForTesting + boolean closeOpenFolder(Folder openFolder) { + if (openFolder != null && openFolder != this) { + // Close any open folder before opening a folder. + openFolder.close(true); + return true; + } + return false; + } + @Override protected boolean isOfType(int type) { return (type & TYPE_FOLDER) != 0; @@ -786,7 +821,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mCurrentAnimator.cancel(); } - if (isEditingName()) { + if (mIsEditingName) { mFolderName.dispatchBackKey(); } @@ -870,7 +905,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (parent != null) { parent.removeView(this); } - mDragController.removeDropTarget(this); + getDragController().removeDropTarget(this); clearFocus(); if (mFolderIcon != null) { mFolderIcon.setVisibility(View.VISIBLE); @@ -891,12 +926,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mRearrangeOnClose = false; } if (getItemCount() <= 1) { - if (!mDragInProgress && !mSuppressFolderDeletion) { + if (!mIsDragInProgress && !mSuppressFolderDeletion) { replaceFolderWithFinalItem(); - } else if (mDragInProgress) { + } else if (mIsDragInProgress) { mDeleteFolderOnDropCompleted = true; } - } else if (!mDragInProgress) { + } else if (!mIsDragInProgress) { mContent.unbindItems(); } mSuppressFolderDeletion = false; @@ -1016,7 +1051,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } - private void clearDragInfo() { + @VisibleForTesting + void clearDragInfo() { mCurrentDragView = null; mIsExternalDrag = false; } @@ -1057,7 +1093,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (getItemCount() <= 1) { mDeleteFolderOnDropCompleted = true; } - if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) { + if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon + && target != this) { replaceFolderWithFinalItem(); } } else { @@ -1088,7 +1125,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } mDeleteFolderOnDropCompleted = false; - mDragInProgress = false; + mIsDragInProgress = false; mItemAddedBackToSelfViaIcon = false; mCurrentDragView = null; @@ -1105,7 +1142,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } private void updateItemLocationsInDatabaseBatch(boolean isBind) { - FolderGridOrganizer verifier = new FolderGridOrganizer( + FolderGridOrganizer verifier = createFolderGridOrganizer( mActivityContext.getDeviceProfile()).setFolderInfo(mInfo); ArrayList items = new ArrayList<>(); @@ -1131,7 +1168,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } public void notifyDrop() { - if (mDragInProgress) { + if (mIsDragInProgress) { mItemAddedBackToSelfViaIcon = true; } } @@ -1174,28 +1211,41 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } protected int getContentAreaHeight() { - DeviceProfile grid = mActivityContext.getDeviceProfile(); - int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y - - mFooterHeight; - int height = Math.min(maxContentAreaHeight, + int height = Math.min(getMaxContentAreaHeight(), mContent.getDesiredHeight()); return Math.max(height, MIN_CONTENT_DIMEN); } - private int getContentAreaWidth() { + @VisibleForTesting + int getMaxContentAreaHeight() { + DeviceProfile grid = mActivityContext.getDeviceProfile(); + return grid.availableHeightPx - grid.getTotalWorkspacePadding().y + - getFooterHeight(); + } + + @VisibleForTesting + int getContentAreaWidth() { return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN); } - private int getFolderWidth() { + @VisibleForTesting + int getFolderWidth() { return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); } - private int getFolderHeight() { + @VisibleForTesting + int getFolderHeight() { return getFolderHeight(getContentAreaHeight()); } - private int getFolderHeight(int contentAreaHeight) { - return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; + @VisibleForTesting + int getFolderHeight(int contentAreaHeight) { + return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight(); + } + + @VisibleForTesting + int getFooterHeight() { + return mFooterHeight; } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -1365,7 +1415,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } // Clear the drag info, as it is no longer being dragged. - mDragInProgress = false; + mIsDragInProgress = false; if (mContent.getPageCount() > 1) { // The animation has already been shown while opening the folder. @@ -1403,7 +1453,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Override public void onAdd(ItemInfo item, int rank) { - FolderGridOrganizer verifier = new FolderGridOrganizer( + FolderGridOrganizer verifier = createFolderGridOrganizer( mActivityContext.getDeviceProfile()).setFolderInfo(mInfo); verifier.updateRankAndPos(item, rank); mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX, @@ -1434,7 +1484,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } - private View getViewForInfo(final ItemInfo item) { + @VisibleForTesting + View getViewForInfo(final ItemInfo item) { return mContent.iterateOverItems((info, view) -> info == item); } @@ -1492,7 +1543,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (hasFocus) { mFromLabelState = mInfo.getFromLabelState(); mFromTitle = mInfo.title; - startEditingFolderName(); + post(this::startEditingFolderName); } else { StatsLogger statsLogger = mStatsLogManager.logger() .withItemInfo(mInfo) @@ -1625,7 +1676,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo /** Navigation bar back key or hardware input back key has been issued. */ @Override public void onBackInvoked() { - if (isEditingName()) { + if (mIsEditingName) { mFolderName.dispatchBackKey(); } else { super.onBackInvoked(); @@ -1637,7 +1688,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (ev.getAction() == MotionEvent.ACTION_DOWN) { BaseDragLayer dl = (BaseDragLayer) getParent(); - if (isEditingName()) { + if (mIsEditingName) { if (!dl.isEventOverView(mFolderName, ev)) { mFolderName.dispatchBackKey(); return true; @@ -1684,6 +1735,95 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return mContent; } + @VisibleForTesting + void setItemAddedBackToSelfViaIcon(boolean value) { + mItemAddedBackToSelfViaIcon = value; + } + + @VisibleForTesting + boolean getItemAddedBackToSelfViaIcon() { + return mItemAddedBackToSelfViaIcon; + } + + @VisibleForTesting + void setIsDragInProgress(boolean value) { + mIsDragInProgress = value; + } + + @VisibleForTesting + boolean getIsDragInProgress() { + return mIsDragInProgress; + } + + @VisibleForTesting + View getCurrentDragView() { + return mCurrentDragView; + } + + @VisibleForTesting + void setCurrentDragView(View view) { + mCurrentDragView = view; + } + + @VisibleForTesting + boolean getItemsInvalidated() { + return mItemsInvalidated; + } + + @VisibleForTesting + void setItemsInvalidated(boolean value) { + mItemsInvalidated = value; + } + + @VisibleForTesting + boolean getIsExternalDrag() { + return mIsExternalDrag; + } + + @VisibleForTesting + void setIsExternalDrag(boolean value) { + mIsExternalDrag = value; + } + + public boolean getIsEditingName() { + return mIsEditingName; + } + + @VisibleForTesting + void setIsEditingName(boolean value) { + mIsEditingName = value; + } + + @VisibleForTesting + void setFolderName(FolderNameEditText value) { + mFolderName = value; + } + + @VisibleForTesting + FolderNameEditText getFolderName() { + return mFolderName; + } + + @VisibleForTesting + boolean getIsOpen() { + return mIsOpen; + } + + @VisibleForTesting + void setIsOpen(boolean value) { + mIsOpen = value; + } + + @VisibleForTesting + boolean getRearrangeOnClose() { + return mRearrangeOnClose; + } + + @VisibleForTesting + void setRearrangeOnClose(boolean value) { + mRearrangeOnClose = value; + } + /** Returns the height of the current folder's bottom edge from the bottom of the screen. */ private int getHeightFromBottom() { BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams(); @@ -1693,6 +1833,16 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return windowBottomPx - folderBottomPx; } + @VisibleForTesting + boolean getDeleteFolderOnDropCompleted() { + return mDeleteFolderOnDropCompleted; + } + + @VisibleForTesting + void setDeleteFolderOnDropCompleted(boolean value) { + mDeleteFolderOnDropCompleted = value; + } + /** * Save this listener for the special case of when we update the state and concurrently * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a @@ -1702,7 +1852,13 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mPriorityOnFolderStateChangedListener = listener; } - private void setState(@FolderState int newState) { + @VisibleForTesting + int getState() { + return mState; + } + + @VisibleForTesting + void setState(@FolderState int newState) { mState = newState; if (mPriorityOnFolderStateChangedListener != null) { mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState); @@ -1714,6 +1870,60 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } + @VisibleForTesting + Alarm getOnExitAlarm() { + return mOnExitAlarm; + } + + @VisibleForTesting + void setOnExitAlarm(Alarm value) { + mOnExitAlarm = value; + } + + @VisibleForTesting + Alarm getReorderAlarm() { + return mReorderAlarm; + } + + @VisibleForTesting + void setReorderAlarm(Alarm value) { + mReorderAlarm = value; + } + + @VisibleForTesting + Alarm getOnScrollHintAlarm() { + return mOnScrollHintAlarm; + } + + @VisibleForTesting + void setOnScrollHintAlarm(Alarm value) { + mOnScrollHintAlarm = value; + } + + @VisibleForTesting + Alarm getScrollPauseAlarm() { + return mScrollPauseAlarm; + } + + @VisibleForTesting + void setScrollPauseAlarm(Alarm value) { + mScrollPauseAlarm = value; + } + + @VisibleForTesting + int getScrollHintDir() { + return mScrollHintDir; + } + + @VisibleForTesting + void setScrollHintDir(int value) { + mScrollHintDir = value; + } + + @VisibleForTesting + int getScrollAreaOffset() { + return mScrollAreaOffset; + } /** * Adds the provided listener to the running list of Folder listeners * {@link #mOnFolderStateChangedListeners} diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java index 98249921c4..588a6dbdae 100644 --- a/src/com/android/launcher3/folder/FolderAnimationManager.java +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -21,6 +21,7 @@ import static android.view.View.ALPHA; import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; +import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -60,6 +61,7 @@ import java.util.List; */ public class FolderAnimationManager { + private static final float EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE = 0.125F; private static final int FOLDER_NAME_ALPHA_DURATION = 32; private static final int LARGE_FOLDER_FOOTER_DURATION = 128; @@ -98,7 +100,7 @@ public class FolderAnimationManager { mContext = folder.getContext(); mDeviceProfile = folder.mActivityContext.getDeviceProfile(); - mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile); + mPreviewVerifier = createFolderGridOrganizer(mDeviceProfile); mIsOpening = isOpening; @@ -158,12 +160,9 @@ public class FolderAnimationManager { mFolder.mFooter.setPivotX(0); mFolder.mFooter.setPivotY(0); - // We want to create a small X offset for the preview items, so that they follow their - // expected path to their final locations. ie. an icon should not move right, if it's final - // location is to its left. This value is arbitrarily defined. - int previewItemOffsetX = (int) (previewSize / 2); + int previewItemOffsetX = 0; if (Utilities.isRtl(mContext.getResources())) { - previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX); + previewItemOffsetX = (int) (lp.width * initialScale - initialSize); } final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale); @@ -239,36 +238,26 @@ public class FolderAnimationManager { play(a, shapeDelegate.createRevealAnimator( mFolder, startRect, endRect, finalRadius, !mIsOpening)); - // Create reveal animator for the folder content (capture the top 4 icons 2x2) - int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x - + mDeviceProfile.folderCellWidthPx * 2; - int rtlExtraWidth = 0; - int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y - + mDeviceProfile.folderCellHeightPx * 2; int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage(); - // In RTL we want to move to the last 2 columns of icons in the folder. if (Utilities.isRtl(mContext.getResources())) { page = (mContent.getPageCount() - 1) - page; - CellLayout clAtPage = mContent.getPageAt(page); - if (clAtPage != null) { - int numExtraRows = clAtPage.getCountX() - 2; - rtlExtraWidth = (int) Math.max(numExtraRows * (mDeviceProfile.folderCellWidthPx - + mDeviceProfile.folderCellLayoutBorderSpacePx.x), rtlExtraWidth); - } } - int left = mContent.getPaddingLeft() + page * lp.width; + int left = page * lp.width; + + int extraRadius = (int) ((mDeviceProfile.folderIconSizePx / initialScale) + * EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE); Rect contentStart = new Rect( - left + rtlExtraWidth, - 0, - left + width + mContent.getPaddingRight() + rtlExtraWidth, - height); + (int) (left + (startRect.left / initialScale)) - extraRadius, + (int) (startRect.top / initialScale) - extraRadius, + (int) (left + (startRect.right / initialScale)) + extraRadius, + (int) (startRect.bottom / initialScale) + extraRadius); Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height); play(a, shapeDelegate.createRevealAnimator( mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening)); // Fade in the folder name, as the text can overlap the icons when grid size is small. - mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f); - play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1), + mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f); + play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1), mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0, mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION); @@ -329,7 +318,7 @@ public class FolderAnimationManager { mFolder.mFooter.setScaleX(1f); mFolder.mFooter.setScaleY(1f); mFolder.mFooter.setTranslationX(0f); - mFolder.mFolderName.setAlpha(1f); + mFolder.getFolderName().setAlpha(1f); mFolder.setClipChildren(mFolderClipChildren); mFolder.setClipToPadding(mFolderClipToPadding); diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java index 593673d4bd..a7ab7b92aa 100644 --- a/src/com/android/launcher3/folder/FolderGridOrganizer.java +++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java @@ -47,12 +47,19 @@ public class FolderGridOrganizer { /** * Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work. */ - public FolderGridOrganizer(DeviceProfile profile) { - mMaxCountX = profile.numFolderColumns; - mMaxCountY = profile.numFolderRows; + public FolderGridOrganizer(int maxCountX, int maxCountY) { + mMaxCountX = maxCountX; + mMaxCountY = maxCountY; mMaxItemsPerPage = mMaxCountX * mMaxCountY; } + /** + * Creates a FolderGridOrganizer for the given DeviceProfile + */ + public static FolderGridOrganizer createFolderGridOrganizer(DeviceProfile profile) { + return new FolderGridOrganizer(profile.numFolderColumns, profile.numFolderRows); + } + /** * Updates the organizer with the provided folder info */ @@ -194,6 +201,7 @@ public class FolderGridOrganizer { int row = rank / mCountX; return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS; } + // If we have less than 4 items do this return rank < MAX_NUM_ITEMS_IN_PREVIEW; } } diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 00636a30a3..de1bcc34db 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -19,6 +19,7 @@ package com.android.launcher3.folder; import static com.android.launcher3.Flags.enableCursorHoverStates; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; +import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY; @@ -82,7 +83,7 @@ import com.android.launcher3.util.Executors; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.Thunk; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.IconLabelDotView; +import com.android.launcher3.views.FloatingIconViewCompanion; import com.android.launcher3.widget.PendingAddShortcutInfo; import java.util.ArrayList; @@ -92,7 +93,7 @@ import java.util.function.Predicate; /** * An icon that can appear on in the workspace representing an {@link Folder}. */ -public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView, +public class FolderIcon extends FrameLayout implements FolderListener, FloatingIconViewCompanion, DraggableView, Reorderable { private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); @@ -223,7 +224,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); - icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile()); + icon.mPreviewVerifier = createFolderGridOrganizer(activity.getDeviceProfile()); icon.mPreviewVerifier.setFolderInfo(folderInfo); icon.updatePreviewItems(false); @@ -458,7 +459,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter()); onTitleChanged(mInfo.title); - mFolder.mFolderName.setText(mInfo.title); + mFolder.getFolderName().setText(mInfo.title); // Logging for folder creation flow StatsLogManager.newInstance(getContext()).logger() diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java index 8eaa0dceab..9dc2d240c4 100644 --- a/src/com/android/launcher3/folder/FolderPagedView.java +++ b/src/com/android/launcher3/folder/FolderPagedView.java @@ -18,6 +18,7 @@ package com.android.launcher3.folder; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER; +import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; import android.annotation.SuppressLint; import android.content.Context; @@ -100,10 +101,21 @@ public class FolderPagedView extends PagedView implements Cli private boolean mViewsBound = false; public FolderPagedView(Context context, AttributeSet attrs) { + this( + context, + attrs, + createFolderGridOrganizer(ActivityContext.lookupContext(context).getDeviceProfile()) + ); + } + + public FolderPagedView( + Context context, + AttributeSet attrs, + FolderGridOrganizer folderGridOrganizer + ) { super(context, attrs); ActivityContext activityContext = ActivityContext.lookupContext(context); - DeviceProfile profile = activityContext.getDeviceProfile(); - mOrganizer = new FolderGridOrganizer(profile); + mOrganizer = folderGridOrganizer; mIsRtl = Utilities.isRtl(getResources()); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -361,8 +373,8 @@ public class FolderPagedView extends PagedView implements Cli // Update footer mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text. - mFolder.mFolderName.setGravity(getPageCount() > 1 ? - (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); + mFolder.getFolderName().setGravity(getPageCount() > 1 + ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); } public int getDesiredWidth() { diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java index 63116384c3..2276ac7d85 100644 --- a/src/com/android/launcher3/folder/PreviewItemManager.java +++ b/src/com/android/launcher3/folder/PreviewItemManager.java @@ -23,6 +23,7 @@ import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_I import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -41,14 +42,13 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Flags; import com.android.launcher3.Utilities; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.apppairs.AppPairIconDrawingParams; import com.android.launcher3.apppairs.AppPairIconGraphic; -import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.model.data.AppPairInfo; 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.util.Themes; import com.android.launcher3.views.ActivityContext; @@ -442,10 +442,8 @@ public class PreviewItemManager { @VisibleForTesting public void setDrawable(PreviewItemDrawingParams p, ItemInfo item) { if (item instanceof WorkspaceItemInfo wii) { - if (wii.hasPromiseIconUi() || (wii.runtimeStatusFlags - & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { - PreloadIconDrawable drawable = newPendingIcon(mContext, wii); - p.drawable = drawable; + if (isActivePendingIcon(wii)) { + p.drawable = newPendingIcon(mContext, wii); } else { p.drawable = wii.newIcon(mContext, Themes.isThemedIconEnabled(mContext) ? FLAG_THEMED : 0); @@ -463,4 +461,14 @@ public class PreviewItemManager { // callback will be released when the folder is opened. p.drawable.setCallback(mIcon); } + + /** + * Returns true if item is a Promise Icon or actively downloading, and the item is not an + * inactive archived app. + */ + private boolean isActivePendingIcon(WorkspaceItemInfo item) { + return (item.hasPromiseIconUi() + || (item.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) + && !(Flags.useNewIconForArchivedApps() && item.isInactiveArchive()); + } } diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java index dc8694d2f6..531cdfd38e 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java @@ -16,11 +16,13 @@ package com.android.launcher3.graphics; import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; +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.Themes.isThemedIconEnabled; import android.content.ContentProvider; import android.content.ContentValues; +import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.MatrixCursor; @@ -32,14 +34,22 @@ import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.Message; import android.os.Messenger; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.InvariantDeviceProfile.GridOption; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherPrefs; +import com.android.launcher3.model.BgDataModel; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.Flags; + +import java.util.concurrent.ExecutionException; /** * Exposes various launcher grid options and allows the caller to change them. @@ -80,8 +90,10 @@ public class GridCustomizationsProvider extends ContentProvider { private static final String KEY_SURFACE_PACKAGE = "surface_package"; private static final String KEY_CALLBACK = "callback"; public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row"; + public static final String KEY_GRID_NAME = "grid_name"; private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337; + private static final int MESSAGE_ID_UPDATE_GRID = 7414; /** * Here we use the IBinder and the screen ID as the key of the active previews. @@ -141,14 +153,20 @@ public class GridCustomizationsProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - switch (uri.getPath()) { + String path = uri.getPath(); + Context context = getContext(); + if (path == null || context == null) { + return 0; + } + switch (path) { case KEY_DEFAULT_GRID: { String gridName = values.getAsString(KEY_NAME); - InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext()); + InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); // Verify that this is a valid grid option GridOption match = null; - for (GridOption option : idp.parseAllGridOptions(getContext())) { - if (option.name.equals(gridName)) { + for (GridOption option : idp.parseAllGridOptions(context)) { + String name = option.name; + if (name != null && name.equals(gridName)) { match = option; break; } @@ -157,15 +175,23 @@ public class GridCustomizationsProvider extends ContentProvider { return 0; } - idp.setCurrentGrid(getContext(), gridName); - getContext().getContentResolver().notifyChange(uri, null); + idp.setCurrentGrid(context, gridName); + if (Flags.newCustomizationPickerUi()) { + try { + // Wait for device profile to be fully reloaded and applied to the launcher + loadModelSync(context); + } catch (ExecutionException | InterruptedException e) { + Log.e(TAG, "Fail to load model", e); + } + } + context.getContentResolver().notifyChange(uri, null); return 1; } case ICON_THEMED: case SET_ICON_THEMED: { - LauncherPrefs.get(getContext()) + LauncherPrefs.get(context) .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE)); - getContext().getContentResolver().notifyChange(uri, null); + context.getContentResolver().notifyChange(uri, null); return 1; } default: @@ -173,6 +199,23 @@ public class GridCustomizationsProvider extends ContentProvider { } } + /** + * Loads the model in memory synchronously + */ + private void loadModelSync(Context context) throws ExecutionException, InterruptedException { + Preconditions.assertNonUiThread(); + BgDataModel.Callbacks emptyCallbacks = new BgDataModel.Callbacks() { }; + LauncherModel launcherModel = LauncherAppState.getInstance(context).getModel(); + MAIN_EXECUTOR.submit( + () -> launcherModel.addCallbacksAndLoad(emptyCallbacks) + ).get(); + + Executors.MODEL_EXECUTOR.submit(() -> { }).get(); + MAIN_EXECUTOR.submit( + () -> launcherModel.removeCallbacks(emptyCallbacks) + ).get(); + } + @Override public Bundle call(String method, String arg, Bundle extras) { if (getContext().checkPermission("android.permission.BIND_WALLPAPER", @@ -224,7 +267,7 @@ public class GridCustomizationsProvider extends ContentProvider { } observer.destroyed = true; observer.renderer.getHostToken().unlinkToDeath(observer, 0); - Executors.MAIN_EXECUTOR.execute(observer.renderer::destroy); + MAIN_EXECUTOR.execute(observer.renderer::destroy); PreviewLifecycleObserver cached = mActivePreviews.get(observer.getIdentifier()); if (cached == observer) { mActivePreviews.remove(observer.getIdentifier()); @@ -245,11 +288,22 @@ public class GridCustomizationsProvider extends ContentProvider { if (destroyed) { return true; } - if (message.what == MESSAGE_ID_UPDATE_PREVIEW) { - renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW)); - } else { - destroyObserver(this); + + switch (message.what) { + case MESSAGE_ID_UPDATE_PREVIEW: + renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW)); + break; + case MESSAGE_ID_UPDATE_GRID: + String gridName = message.getData().getString(KEY_GRID_NAME); + if (!TextUtils.isEmpty(gridName)) { + renderer.updateGrid(gridName); + } + break; + default: + destroyObserver(this); + break; } + return true; } diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java index 778b32a863..00f1c675da 100644 --- a/src/com/android/launcher3/graphics/IconPalette.java +++ b/src/com/android/launcher3/graphics/IconPalette.java @@ -16,22 +16,15 @@ package com.android.launcher3.graphics; -import android.app.Notification; import android.content.Context; import android.graphics.Color; -import android.util.Log; -import androidx.core.graphics.ColorUtils; - -import com.android.launcher3.R; import com.android.launcher3.util.Themes; /** * Contains colors based on the dominant color of an icon. */ public class IconPalette { - - private static final boolean DEBUG = false; private static final String TAG = "IconPalette"; private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f; @@ -54,95 +47,4 @@ public class IconPalette { } return result; } - - /** - * Resolves a color such that it has enough contrast to be used as the - * color of an icon or text on the given background color. - * - * @return a color of the same hue with enough contrast against the background. - * - * This was copied from com.android.internal.util.NotificationColorUtil. - */ - public static int resolveContrastColor(Context context, int color, int background) { - final int resolvedColor = resolveColor(context, color); - - int contrastingColor = ensureTextContrast(resolvedColor, background); - - if (contrastingColor != resolvedColor) { - if (DEBUG){ - Log.w(TAG, String.format( - "Enhanced contrast of notification for %s " + - "%s (over background) by changing #%s to %s", - context.getPackageName(), - contrastChange(resolvedColor, contrastingColor, background), - Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor))); - } - } - return contrastingColor; - } - - /** - * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} - * - * This was copied from com.android.internal.util.NotificationColorUtil. - */ - private static int resolveColor(Context context, int color) { - if (color == Notification.COLOR_DEFAULT) { - return context.getColor(R.color.notification_icon_default_color); - } - return color; - } - - /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */ - private static String contrastChange(int colorOld, int colorNew, int bg) { - return String.format("from %.2f:1 to %.2f:1", - ColorUtils.calculateContrast(colorOld, bg), - ColorUtils.calculateContrast(colorNew, bg)); - } - - /** - * Finds a text color with sufficient contrast over bg that has the same hue as the original - * color. - * - * This was copied from com.android.internal.util.NotificationColorUtil. - */ - private static int ensureTextContrast(int color, int bg) { - return findContrastColor(color, bg, 4.5); - } - /** - * Finds a suitable color such that there's enough contrast. - * - * @param fg the color to start searching from. - * @param bg the color to ensure contrast against. - * @param minRatio the minimum contrast ratio required. - * @return a color with the same hue as {@param color}, potentially darkened to meet the - * contrast ratio. - * - * This was copied from com.android.internal.util.NotificationColorUtil. - */ - private static int findContrastColor(int fg, int bg, double minRatio) { - if (ColorUtils.calculateContrast(fg, bg) >= minRatio) { - return fg; - } - - double[] lab = new double[3]; - ColorUtils.colorToLAB(bg, lab); - double bgL = lab[0]; - ColorUtils.colorToLAB(fg, lab); - double fgL = lab[0]; - boolean isBgDark = bgL < 50; - - double low = isBgDark ? fgL : 0, high = isBgDark ? 100 : fgL; - final double a = lab[1], b = lab[2]; - for (int i = 0; i < 15 && high - low > 0.00001; i++) { - final double l = (low + high) / 2; - fg = ColorUtils.LABToColor(l, a, b); - if (ColorUtils.calculateContrast(fg, bg) > minRatio) { - if (isBgDark) high = l; else low = l; - } else { - if (isBgDark) low = l; else high = l; - } - } - return ColorUtils.LABToColor(low, a, b); - } } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index ae8f1d513c..40c0cc65c9 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -23,6 +23,7 @@ import static android.view.View.VISIBLE; import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; import static com.android.launcher3.BubbleTextView.DISPLAY_WORKSPACE; import static com.android.launcher3.DeviceProfile.DEFAULT_SCALE; +import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_PREVIEW_RENDERER; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; @@ -68,7 +69,6 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.WorkspaceLayoutManager; import com.android.launcher3.apppairs.AppPairIcon; @@ -78,8 +78,6 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.WidgetItem; -import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; @@ -106,6 +104,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile. @@ -207,15 +206,12 @@ public class LauncherPreviewRenderer extends ContextWrapper mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel); } - if (Utilities.ATLEAST_S) { - WallpaperColors wallpaperColors = wallpaperColorsOverride != null - ? wallpaperColorsOverride - : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM); - mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance( - context).generateColorsOverride(wallpaperColors) : null; - } else { - mWallpaperColorResources = null; - } + WallpaperColors wallpaperColors = wallpaperColorsOverride != null + ? wallpaperColorsOverride + : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM); + mWallpaperColorResources = wallpaperColors != null + ? LocalColorExtractor.newInstance(context).generateColorsOverride(wallpaperColors) + : null; mAppWidgetHost = new LauncherPreviewAppWidgetHost(context); } @@ -321,12 +317,12 @@ public class LauncherPreviewRenderer extends ContextWrapper mUiHandler.post(() -> { if (mDp.isTaskbarPresent) { // hotseat icons on bottom - mHotseat.setIconsAlpha(hide ? 0 : 1); + mHotseat.setIconsAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); if (mDp.isQsbInline) { - mHotseat.setQsbAlpha(hide ? 0 : 1); + mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); } } else { - mHotseat.setQsbAlpha(hide ? 0 : 1); + mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); } }); } @@ -376,15 +372,6 @@ public class LauncherPreviewRenderer extends ContextWrapper getApplicationContext(), providerInfo)); } - private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) { - WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName( - info.providerName, info.user, mContext); - if (widgetItem == null) { - return; - } - inflateAndAddWidgets(info, widgetItem.widgetInfo); - } - private void inflateAndAddWidgets( LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) { AppWidgetHostView view = mAppWidgetHost.createView( @@ -468,17 +455,22 @@ public class LauncherPreviewRenderer extends ContextWrapper break; } } + Map widgetsMap = widgetProviderInfoMap; for (ItemInfo itemInfo : currentAppWidgets) { switch (itemInfo.itemType) { case Favorites.ITEM_TYPE_APPWIDGET: case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: - if (widgetProviderInfoMap != null) { - inflateAndAddWidgets( - (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap); - } else { - inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, - dataModel.widgetsModel); + if (widgetsMap == null) { + widgetsMap = dataModel.widgetsModel.getWidgetsByComponentKey() + .entrySet() + .stream() + .filter(entry -> entry.getValue().widgetInfo != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().widgetInfo + )); } + inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, widgetsMap); break; default: break; @@ -508,6 +500,7 @@ public class LauncherPreviewRenderer extends ContextWrapper && !SHOULD_SHOW_FIRST_PAGE_WIDGET) { CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID); View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false); + // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes. CellLayoutLayoutParams lp = new CellLayoutLayoutParams( 0, 0, firstScreen.getCountX(), 1); lp.canReorder = false; diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index addd0727e8..56c4ca4b37 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -38,6 +38,7 @@ import android.view.SurfaceControlViewHost; import android.view.SurfaceControlViewHost.SurfacePackage; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -61,6 +62,7 @@ import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.Themes; import com.android.launcher3.widget.LocalColorExtractor; +import com.android.systemui.shared.Flags; import java.util.ArrayList; import java.util.Map; @@ -96,6 +98,7 @@ public class PreviewSurfaceRenderer { private boolean mDestroyed = false; private LauncherPreviewRenderer mRenderer; private boolean mHideQsb; + @Nullable private FrameLayout mViewRoot = null; public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception { mContext = context; @@ -193,6 +196,19 @@ public class PreviewSurfaceRenderer { MODEL_EXECUTOR.execute(this::loadModelData); } + /** + * Update the grid of the launcher preview + * + * @param gridName Name of the grid, e.g. normal, practical + */ + public void updateGrid(@NonNull String gridName) { + if (gridName.equals(mGridName)) { + return; + } + mGridName = gridName; + loadAsync(); + } + /** * Hides the components in the bottom row. * @@ -302,11 +318,41 @@ public class PreviewSurfaceRenderer { view.setPivotY(0); view.setTranslationX((mWidth - scale * view.getWidth()) / 2); view.setTranslationY((mHeight - scale * view.getHeight()) / 2); - view.setAlpha(0); - view.animate().alpha(1) - .setInterpolator(new AccelerateDecelerateInterpolator()) - .setDuration(FADE_IN_ANIMATION_DURATION) - .start(); - mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight()); + if (!Flags.newCustomizationPickerUi()) { + view.setAlpha(0); + view.animate().alpha(1) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .setDuration(FADE_IN_ANIMATION_DURATION) + .start(); + mSurfaceControlViewHost.setView( + view, + view.getMeasuredWidth(), + view.getMeasuredHeight() + ); + return; + } + + if (mViewRoot == null) { + mViewRoot = new FrameLayout(inflationContext); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, // Width + FrameLayout.LayoutParams.WRAP_CONTENT // Height + ); + mViewRoot.setLayoutParams(layoutParams); + mViewRoot.addView(view); + mViewRoot.setAlpha(0); + mViewRoot.animate().alpha(1) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .setDuration(FADE_IN_ANIMATION_DURATION) + .start(); + mSurfaceControlViewHost.setView( + mViewRoot, + view.getMeasuredWidth(), + view.getMeasuredHeight() + ); + } else { + mViewRoot.removeAllViews(); + mViewRoot.addView(view); + } } } diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java index 260d490926..077ddfc665 100644 --- a/src/com/android/launcher3/graphics/SysUiScrim.java +++ b/src/com/android/launcher3/graphics/SysUiScrim.java @@ -18,8 +18,6 @@ package com.android.launcher3.graphics; import static android.graphics.Paint.DITHER_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; -import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION; - import android.animation.ObjectAnimator; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -111,7 +109,7 @@ public class SysUiScrim implements View.OnAttachStateChangeListener { new int[]{0x00FFFFFF, 0x2FFFFFFF}, new float[]{0f, 1f}); - if (!KEYGUARD_ANIMATION.get() && !mHideSysUiScrim) { + if (!mHideSysUiScrim) { view.addOnAttachStateChangeListener(this); } } diff --git a/src/com/android/launcher3/icons/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java deleted file mode 100644 index 30575fcbe5..0000000000 --- a/src/com/android/launcher3/icons/ComponentWithLabel.java +++ /dev/null @@ -1,75 +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.launcher3.icons; - -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.UserHandle; - -import androidx.annotation.NonNull; - -import com.android.launcher3.icons.cache.CachingLogic; - -public interface ComponentWithLabel { - - ComponentName getComponent(); - - UserHandle getUser(); - - CharSequence getLabel(PackageManager pm); - - - class ComponentCachingLogic implements CachingLogic { - - private final PackageManager mPackageManager; - private final boolean mAddToMemCache; - - public ComponentCachingLogic(Context context, boolean addToMemCache) { - mPackageManager = context.getPackageManager(); - mAddToMemCache = addToMemCache; - } - - @Override - @NonNull - public ComponentName getComponent(@NonNull T object) { - return object.getComponent(); - } - - @NonNull - @Override - public UserHandle getUser(@NonNull T object) { - return object.getUser(); - } - - @NonNull - @Override - public CharSequence getLabel(@NonNull T object) { - return object.getLabel(mPackageManager); - } - - @NonNull - @Override - public BitmapInfo loadIcon(@NonNull Context context, @NonNull T object) { - return BitmapInfo.LOW_RES_INFO; - } - - @Override - public boolean addToMemCache() { - return mAddToMemCache; - } - } -} diff --git a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java deleted file mode 100644 index 0a52dd7191..0000000000 --- a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.icons; - -import android.content.Context; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; - -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.icons.BaseIconFactory.IconOptions; - -/** - * Extension of ComponentWithLabel to also support loading icons - */ -public interface ComponentWithLabelAndIcon extends ComponentWithLabel { - - /** - * Provide an icon for this object - */ - Drawable getFullResIcon(IconCache cache); - - class ComponentWithIconCachingLogic extends ComponentCachingLogic { - - public ComponentWithIconCachingLogic(Context context, boolean addToMemCache) { - super(context, addToMemCache); - } - - @NonNull - @Override - public BitmapInfo loadIcon(@NonNull Context context, - @NonNull ComponentWithLabelAndIcon object) { - Drawable d = object.getFullResIcon(LauncherAppState.getInstance(context) - .getIconCache()); - if (d == null) { - return super.loadIcon(context, object); - } - try (LauncherIcons li = LauncherIcons.obtain(context)) { - return li.createBadgedIconBitmap(d, new IconOptions().setUser(object.getUser())); - } - } - } -} diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 44e448eea1..587dc2731a 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -54,9 +54,10 @@ import androidx.core.util.Pair; import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; -import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic; import com.android.launcher3.icons.cache.BaseIconCache; +import com.android.launcher3.icons.cache.CachedObjectCachingLogic; import com.android.launcher3.icons.cache.CachingLogic; +import com.android.launcher3.icons.cache.LauncherActivityCachingLogic; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.IconRequestInfo; @@ -102,7 +103,6 @@ public class IconCache extends BaseIconCache { private final LauncherApps mLauncherApps; private final UserCache mUserManager; private final InstantAppResolver mInstantAppResolver; - private final IconProvider mIconProvider; private final CancellableTask mCancelledTask; private final SparseArray mWidgetCategoryBitmapInfos; @@ -112,14 +112,14 @@ public class IconCache extends BaseIconCache { public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName, IconProvider iconProvider) { super(context, dbFileName, MODEL_EXECUTOR.getLooper(), - idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */); - mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false); - mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context); + idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider); + mComponentWithLabelCachingLogic = new CachedObjectCachingLogic( + context, false /* loadIcons */, false /* addToMemCache */); + mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.INSTANCE; mShortcutCachingLogic = new ShortcutCachingLogic(); mLauncherApps = mContext.getSystemService(LauncherApps.class); mUserManager = UserCache.INSTANCE.get(mContext); mInstantAppResolver = InstantAppResolver.newInstance(mContext); - mIconProvider = iconProvider; mWidgetCategoryBitmapInfos = new SparseArray<>(); mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { }); @@ -337,6 +337,9 @@ public class IconCache extends BaseIconCache { } } + /** + * Loads and returns the icon for the provided object without adding it to memCache + */ public synchronized String getTitleNoCache(ComponentWithLabel info) { CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info, mComponentWithLabelCachingLogic, false /* usePackageIcon */, @@ -629,12 +632,6 @@ public class IconCache extends BaseIconCache { info.getAppLabel()); } - @Override - @NonNull - protected String getIconSystemState(String packageName) { - return mIconProvider.getSystemStateForPackage(mSystemState, packageName); - } - /** * Interface for receiving itemInfo with high-res icon. */ diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java deleted file mode 100644 index 406f69758c..0000000000 --- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java +++ /dev/null @@ -1,72 +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.launcher3.icons; - -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.LauncherActivityInfo; -import android.os.UserHandle; - -import androidx.annotation.NonNull; - -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.R; -import com.android.launcher3.icons.BaseIconFactory.IconOptions; -import com.android.launcher3.icons.cache.CachingLogic; -import com.android.launcher3.util.ResourceBasedOverride; - -/** - * Caching logic for LauncherActivityInfo. - */ -public class LauncherActivityCachingLogic - implements CachingLogic, ResourceBasedOverride { - - /** - * Creates and returns a new instance - */ - public static LauncherActivityCachingLogic newInstance(Context context) { - return Overrides.getObject(LauncherActivityCachingLogic.class, context, - R.string.launcher_activity_logic_class); - } - - @NonNull - @Override - public ComponentName getComponent(@NonNull LauncherActivityInfo object) { - return object.getComponentName(); - } - - @NonNull - @Override - public UserHandle getUser(@NonNull LauncherActivityInfo object) { - return object.getUser(); - } - - @NonNull - @Override - public CharSequence getLabel(@NonNull LauncherActivityInfo object) { - return object.getLabel(); - } - - @NonNull - @Override - public BitmapInfo loadIcon(@NonNull Context context, @NonNull LauncherActivityInfo object) { - try (LauncherIcons li = LauncherIcons.obtain(context)) { - return li.createBadgedIconBitmap(LauncherAppState.getInstance(context) - .getIconProvider().getIcon(object, li.mFillResIconDpi), - new IconOptions().setUser(object.getUser())); - } - } -} diff --git a/src/com/android/launcher3/icons/Legacy.kt b/src/com/android/launcher3/icons/Legacy.kt new file mode 100644 index 0000000000..3bf3bb2539 --- /dev/null +++ b/src/com/android/launcher3/icons/Legacy.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.launcher3.icons + +import com.android.launcher3.icons.cache.CachedObject + +/** + * This files contains some definitions used during refactoring to avoid breaking changes. + * + * TODO(b/366237794) remove this file once refactoring is complete + */ + +/** Temporary interface to allow easier refactoring */ +interface ComponentWithLabel : CachedObject + +/** Temporary interface to allow easier refactoring */ +interface ComponentWithLabelAndIcon : ComponentWithLabel diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java index f40eda6c2d..7bb39e1230 100644 --- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java +++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.LauncherAppState; import com.android.launcher3.icons.BaseIconFactory.IconOptions; +import com.android.launcher3.icons.cache.BaseIconCache; import com.android.launcher3.icons.cache.CachingLogic; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.Themes; @@ -72,7 +73,8 @@ public class ShortcutCachingLogic implements CachingLogic { @NonNull @Override - public BitmapInfo loadIcon(@NonNull Context context, @NonNull ShortcutInfo info) { + public BitmapInfo loadIcon(@NonNull Context context, @NonNull BaseIconCache cache, + @NonNull ShortcutInfo info) { try (LauncherIcons li = LauncherIcons.obtain(context)) { Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon( context, info, LauncherAppState.getIDP(context).fillResIconDpi); diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java index 480e8f3e56..ec0efe0244 100644 --- a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java +++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java @@ -199,6 +199,10 @@ public abstract class ItemFocusIndicatorHelper implements AnimatorUpdateListe } protected void changeFocus(T item, boolean hasFocus) { + if (mLastFocusedItem != item && !hasFocus) { + return; + } + if (hasFocus) { endCurrentAnimation(); diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index 861631dbed..fbd24d89f0 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -795,6 +795,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User launches Overview from meta+tab keyboard shortcut") LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT(1765), + @UiEvent(doc = "User long pressed on the taskbar IME switcher button") + LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798), + // ADD MORE ; diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java index 64ebbf3833..1f60f132df 100644 --- a/src/com/android/launcher3/model/AllAppsList.java +++ b/src/com/android/launcher3/model/AllAppsList.java @@ -223,7 +223,8 @@ public class AllAppsList { if (DEBUG) { Log.w(TAG, "updatePromiseInstallInfo: removing app due to install" + " failure and appInfo not startable." - + " package=" + appInfo.getTargetPackage()); + + " package=" + appInfo.getTargetPackage() + + ", user=" + user); } removeApp(i); } @@ -319,7 +320,8 @@ public class AllAppsList { if (!findActivity(matches, applicationInfo.componentName)) { if (DEBUG) { Log.w(TAG, "Changing shortcut target due to app component name change." - + " package=" + packageName); + + " component=" + applicationInfo.componentName + + ", user=" + user); } removeApp(i); } @@ -346,8 +348,9 @@ public class AllAppsList { } else { // Remove all data for this package. if (DEBUG) { - Log.w(TAG, "updatePromiseInstallInfo: no Activities matched updated package," - + " removing all apps from package=" + packageName); + Log.w(TAG, "updatePackage: no Activities matched updated package," + + " removing any AppInfo with package=" + packageName + + ", user=" + user); } for (int i = data.size() - 1; i >= 0; i--) { final AppInfo applicationInfo = data.get(i); diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java index e6ade610f7..5faa2b83c3 100644 --- a/src/com/android/launcher3/model/BaseLauncherBinder.java +++ b/src/com/android/launcher3/model/BaseLauncherBinder.java @@ -52,6 +52,7 @@ import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.RunnableList; +import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.util.ArrayList; @@ -195,8 +196,8 @@ public class BaseLauncherBinder { if (!WIDGETS_ENABLED) { return; } - final List widgets = - mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext()); + List widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext()) + .build(mBgDataModel.widgetsModel.getWidgetsByPackageItem()); executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor); } diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java index ad32fc2828..942b97cb19 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java +++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java @@ -28,8 +28,6 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; @@ -47,7 +45,6 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.GridOccupancy; @@ -100,7 +97,7 @@ public class GridSizeMigrationUtil { @VisibleForTesting public static List readAllEntries(SQLiteDatabase db, String tableName, Context context) { - DbReader dbReader = new DbReader(db, tableName, context, getValidPackages(context)); + DbReader dbReader = new DbReader(db, tableName, context); List result = dbReader.loadAllWorkspaceEntries(); result.addAll(dbReader.loadHotseatEntries()); return result; @@ -121,13 +118,21 @@ public class GridSizeMigrationUtil { @NonNull DeviceGridState destDeviceState, @NonNull DatabaseHelper target, @NonNull SQLiteDatabase source) { + + Log.i("b/360462379", "Going from " + srcDeviceState.getColumns() + "x" + + srcDeviceState.getRows()); + Log.i("b/360462379", "Going to " + destDeviceState.getColumns() + "x" + + destDeviceState.getRows()); + if (!needsToMigrate(srcDeviceState, destDeviceState)) { + Log.i("b/360462379", "Does not need to migrate."); return true; } if (Flags.enableGridMigrationFix() && srcDeviceState.getColumns().equals(destDeviceState.getColumns()) && srcDeviceState.getRows() < destDeviceState.getRows()) { + Log.i("b/360462379", "Grid migration fix entry point."); // Only use this strategy when comparing the previous grid to the new grid and the // columns are the same and the destination has more rows copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context); @@ -136,11 +141,10 @@ public class GridSizeMigrationUtil { } copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context); - HashSet validPackages = getValidPackages(context); long migrationStartTime = System.currentTimeMillis(); try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) { - DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context, validPackages); - DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context, validPackages); + DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context); + DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context); Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows()); migrate(target, srcReader, destReader, destDeviceState.getNumHotseat(), @@ -213,9 +217,13 @@ public class GridSizeMigrationUtil { Collections.sort(hotseatToBeAdded); Collections.sort(workspaceToBeAdded); + List idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect( + Collectors.toList()); + idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList()); + // Migrate hotseat solveHotseatPlacement(helper, destHotseatSize, - srcReader, destReader, dstHotseatItems, hotseatToBeAdded); + srcReader, destReader, dstHotseatItems, hotseatToBeAdded, idsInUse); // Migrate workspace. // First we create a collection of the screens @@ -230,7 +238,7 @@ public class GridSizeMigrationUtil { Log.d(TAG, "Migrating " + screenId); } solveGridPlacement(helper, srcReader, - destReader, screenId, trgX, trgY, workspaceToBeAdded); + destReader, screenId, trgX, trgY, workspaceToBeAdded, idsInUse); if (workspaceToBeAdded.isEmpty()) { break; } @@ -241,7 +249,7 @@ public class GridSizeMigrationUtil { int screenId = destReader.mLastScreenId + 1; while (!workspaceToBeAdded.isEmpty()) { solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY, - workspaceToBeAdded); + workspaceToBeAdded, srcWorkspaceItems.stream().map(entry -> entry.id).toList()); screenId++; } @@ -257,47 +265,57 @@ public class GridSizeMigrationUtil { private static void calcDiff(@NonNull final List src, @NonNull final List dest, @NonNull final List toBeAdded, @NonNull final IntArray toBeRemoved) { + HashMap entryCountDiff = new HashMap<>(); + src.forEach(entry -> + entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1)); + dest.forEach(entry -> + entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1)); + src.forEach(entry -> { - if (!dest.contains(entry)) { + if (entryCountDiff.get(entry) > 0) { toBeAdded.add(entry); + entryCountDiff.put(entry, entryCountDiff.get(entry) - 1); } }); + dest.forEach(entry -> { - if (!src.contains(entry)) { + if (entryCountDiff.get(entry) < 0) { toBeRemoved.add(entry.id); if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add)); } + entryCountDiff.put(entry, entryCountDiff.get(entry) + 1); } }); } private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry, - String srcTableName, String destTableName) { - int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName); - + String srcTableName, String destTableName, List idsInUse) { + int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName, idsInUse); if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) { for (Set itemIds : entry.mFolderItems.values()) { for (int itemId : itemIds) { - copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName); + copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName, idsInUse); } } } } private static int copyEntryAndUpdate(DatabaseHelper helper, - DbEntry entry, String srcTableName, String destTableName) { - return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName); + DbEntry entry, String srcTableName, String destTableName, List idsInUse) { + return copyEntryAndUpdate( + helper, entry, -1, -1, srcTableName, destTableName, idsInUse); } - private static int copyEntryAndUpdate(DatabaseHelper helper, - int id, int folderId, String srcTableName, String destTableName) { - return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName); + private static int copyEntryAndUpdate(DatabaseHelper helper, int id, + int folderId, String srcTableName, String destTableName, List idsInUse) { + return copyEntryAndUpdate( + helper, null, id, folderId, srcTableName, destTableName, idsInUse); } - private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, - int id, int folderId, String srcTableName, String destTableName) { + private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, int id, + int folderId, String srcTableName, String destTableName, List idsInUse) { int newId = -1; Cursor c = helper.getWritableDatabase().query(srcTableName, null, LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'", @@ -311,6 +329,9 @@ public class GridSizeMigrationUtil { values.put(LauncherSettings.Favorites.CONTAINER, folderId); } newId = helper.generateNewItemId(); + while (idsInUse.contains(newId)) { + newId = helper.generateNewItemId(); + } values.put(LauncherSettings.Favorites._ID, newId); helper.getWritableDatabase().insert(destTableName, null, values); } @@ -323,27 +344,10 @@ public class GridSizeMigrationUtil { Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null); } - private static HashSet getValidPackages(Context context) { - // Initialize list of valid packages. This contain all the packages which are already on - // the device and packages which are being installed. Any item which doesn't belong to - // this set is removed. - // Since the loader removes such items anyway, removing these items here doesn't cause - // any extra data loss and gives us more free space on the grid for better migration. - HashSet validPackages = new HashSet<>(); - for (PackageInfo info : context.getPackageManager() - .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { - validPackages.add(info.packageName); - } - InstallSessionHelper.INSTANCE.get(context) - .getActiveSessions().keySet() - .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName)); - return validPackages; - } - private static void solveGridPlacement(@NonNull final DatabaseHelper helper, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, final int screenId, final int trgX, final int trgY, - @NonNull final List sortedItemsToPlace) { + @NonNull final List sortedItemsToPlace, List idsInUse) { final GridOccupancy occupied = new GridOccupancy(trgX, trgY); final Point trg = new Point(trgX, trgY); final Point next = new Point(0, screenId == 0 @@ -366,7 +370,8 @@ public class GridSizeMigrationUtil { continue; } if (findPlacementForEntry(entry, next, trg, occupied, screenId)) { - insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName); + insertEntryInDb( + helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse); iterator.remove(); } } @@ -407,7 +412,7 @@ public class GridSizeMigrationUtil { @NonNull final DatabaseHelper helper, final int hotseatSize, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, @NonNull final List placedHotseatItems, - @NonNull final List itemsToPlace) { + @NonNull final List itemsToPlace, List idsInUse) { final boolean[] occupied = new boolean[hotseatSize]; for (DbEntry entry : placedHotseatItems) { @@ -422,7 +427,8 @@ public class GridSizeMigrationUtil { // to something other than -1. entry.cellX = i; entry.cellY = 0; - insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName); + insertEntryInDb( + helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse); occupied[entry.screenId] = true; } } @@ -434,18 +440,15 @@ public class GridSizeMigrationUtil { private final SQLiteDatabase mDb; private final String mTableName; private final Context mContext; - private final Set mValidPackages; private int mLastScreenId = -1; private final Map> mWorkspaceEntriesByScreenId = new ArrayMap<>(); - public DbReader(SQLiteDatabase db, String tableName, Context context, - Set validPackages) { + public DbReader(SQLiteDatabase db, String tableName, Context context) { mDb = db; mTableName = tableName; mContext = context; - mValidPackages = validPackages; } protected List loadHotseatEntries() { @@ -477,7 +480,6 @@ public class GridSizeMigrationUtil { case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: { entry.mIntent = c.getString(indexIntent); - verifyIntent(c.getString(indexIntent)); break; } case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: { @@ -559,17 +561,15 @@ public class GridSizeMigrationUtil { case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: { entry.mIntent = c.getString(indexIntent); - verifyIntent(entry.mIntent); break; } case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: { entry.mProvider = c.getString(indexAppWidgetProvider); + entry.appWidgetId = c.getInt(indexAppWidgetId); ComponentName cn = ComponentName.unflattenFromString(entry.mProvider); - verifyPackage(cn.getPackageName()); - int widgetId = c.getInt(indexAppWidgetId); LauncherAppWidgetProviderInfo pInfo = widgetManagerHelper - .getLauncherAppWidgetInfo(widgetId, cn); + .getLauncherAppWidgetInfo(entry.appWidgetId, cn); Point spans = null; if (pInfo != null) { spans = pInfo.getMinSpans(); @@ -629,7 +629,6 @@ public class GridSizeMigrationUtil { try { int id = c.getInt(0); String intent = c.getString(1); - verifyIntent(intent); total++; if (!entry.mFolderItems.containsKey(intent)) { entry.mFolderItems.put(intent, new HashSet<>()); @@ -646,27 +645,6 @@ public class GridSizeMigrationUtil { private Cursor queryWorkspace(String[] columns, String where) { return mDb.query(mTableName, columns, where, null, null, null, null); } - - /** Verifies if the mIntent should be restored. */ - private void verifyIntent(String intentStr) - throws Exception { - Intent intent = Intent.parseUri(intentStr, 0); - if (intent.getComponent() != null) { - verifyPackage(intent.getComponent().getPackageName()); - } else if (intent.getPackage() != null) { - // Only verify package if the component was null. - verifyPackage(intent.getPackage()); - } - } - - /** Verifies if the package should be restored */ - private void verifyPackage(String packageName) - throws Exception { - if (!mValidPackages.contains(packageName)) { - // TODO(b/151468819): Handle promise app icon restoration during grid migration. - throw new Exception("Package not available"); - } - } } public static class DbEntry extends ItemInfo implements Comparable { diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java index 551c2d8ba8..59d1d00204 100644 --- a/src/com/android/launcher3/model/ItemInstallQueue.java +++ b/src/com/android/launcher3/model/ItemInstallQueue.java @@ -192,22 +192,18 @@ public class ItemInstallQueue implements SafeCloseable { } private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) { - final Exception stackTrace = new Exception(); // Queue the item up for adding if launcher has not loaded properly yet MODEL_EXECUTOR.post(() -> { Pair itemInfo = info.getItemInfo(mContext); if (itemInfo == null) { FileLog.d(LOG, - "Adding PendingInstallShortcutInfo with no attached info to queue.", - stackTrace); + "Adding PendingInstallShortcutInfo with no attached info to queue."); } else { FileLog.d(LOG, - "Adding PendingInstallShortcutInfo to queue. Attached info: " - + itemInfo.first, - stackTrace); + "Adding PendingInstallShortcutInfo to queue." + + " Attached info: " + itemInfo.first); } - addToQueue(info); }); flushInstallQueue(); diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 312c6f412a..609846f7fc 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -70,11 +70,11 @@ import com.android.launcher3.folder.FolderGridOrganizer; import com.android.launcher3.folder.FolderNameInfos; import com.android.launcher3.folder.FolderNameProvider; import com.android.launcher3.icons.ComponentWithLabelAndIcon; -import com.android.launcher3.icons.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.icons.LauncherActivityCachingLogic; import com.android.launcher3.icons.ShortcutCachingLogic; +import com.android.launcher3.icons.cache.CachedObjectCachingLogic; import com.android.launcher3.icons.cache.IconCacheUpdateHandler; +import com.android.launcher3.icons.cache.LauncherActivityCachingLogic; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.AppPairInfo; @@ -209,7 +209,10 @@ public class LoaderTask implements Runnable { mApp.getContext().getContentResolver(), "launcher_broadcast_installed_apps", /* def= */ 0); - if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) { + boolean shouldAttachArchivingExtras = mIsRestoreFromBackup + && (launcherBroadcastInstalledApps == 1 + || Flags.enableFirstScreenBroadcastArchivingExtras()); + if (shouldAttachArchivingExtras) { List broadcastModels = FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( mPmHelper, @@ -295,7 +298,7 @@ public class LoaderTask implements Runnable { IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler(); setIgnorePackages(updateHandler); updateHandler.updateIcons(allActivityList, - LauncherActivityCachingLogic.newInstance(mApp.getContext()), + LauncherActivityCachingLogic.INSTANCE, mApp.getModel()::onPackageIconsUpdated); logASplit("update icon cache"); @@ -357,7 +360,7 @@ public class LoaderTask implements Runnable { } updateHandler.updateIcons(allWidgetsList, - new ComponentWithIconCachingLogic(mApp.getContext(), true), + new CachedObjectCachingLogic(mApp.getContext()), mApp.getModel()::onWidgetLabelsUpdated); logASplit("save widgets in icon cache"); @@ -568,7 +571,7 @@ public class LoaderTask implements Runnable { private void processFolderItems() { // Sort the folder items, update ranks, and make sure all preview items are high res. List verifiers = mApp.getInvariantDeviceProfile().supportedProfiles - .stream().map(FolderGridOrganizer::new).toList(); + .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList(); for (CollectionInfo collection : mBgDataModel.collections) { if (!(collection instanceof FolderInfo folder)) { continue; diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java index 7e1d40d96b..da1a221d5d 100644 --- a/src/com/android/launcher3/model/ModelDbController.java +++ b/src/com/android/launcher3/model/ModelDbController.java @@ -83,6 +83,7 @@ import com.android.launcher3.widget.LauncherWidgetHolder; import org.xmlpull.v1.XmlPullParser; +import java.io.File; import java.io.InputStream; import java.io.StringReader; @@ -104,10 +105,30 @@ public class ModelDbController { mContext = context; } + private void printDBs(String prefix) { + try { + File directory = new File( + mContext.getDatabasePath(InvariantDeviceProfile.INSTANCE.get(mContext).dbFile) + .getParent() + ); + if (directory.exists()) { + for (File file : directory.listFiles()) { + Log.d("b/353505773", prefix + "Database file: " + file.getName()); + } + } else { + Log.d("b/353505773", prefix + "No files found in the database directory"); + } + } catch (Exception e) { + Log.e("b/353505773", prefix + e.getMessage()); + } + } + private synchronized void createDbIfNotExists() { if (mOpenHelper == null) { mOpenHelper = createDatabaseHelper(false /* forMigration */); + printDBs("before: "); RestoreDbTask.restoreIfNeeded(mContext, this); + printDBs("after: "); } } diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt index b12b2bc9d2..2ee5b8092f 100644 --- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt +++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt @@ -38,6 +38,7 @@ class ModelLauncherCallbacks(private var taskExecutor: Consumer LauncherApps.Callback() { override fun onPackageAdded(packageName: String, user: UserHandle) { + FileLog.d(TAG, "onPackageAdded triggered for packageName=$packageName, user=$user") taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName)) } @@ -54,7 +55,7 @@ class ModelLauncherCallbacks(private var taskExecutor: Consumer } override fun onPackageRemoved(packageName: String, user: UserHandle) { - FileLog.d(TAG, "package removed received $packageName") + FileLog.d(TAG, "onPackageRemoved triggered for packageName=$packageName, user=$user") taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName)) } diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt index 266ed0c67a..cf2cadc094 100644 --- a/src/com/android/launcher3/model/ModelTaskController.kt +++ b/src/com/android/launcher3/model/ModelTaskController.kt @@ -24,6 +24,7 @@ import com.android.launcher3.model.BgDataModel.FixedContainerItems import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder import java.util.Objects import java.util.concurrent.Executor import java.util.function.Predicate @@ -78,7 +79,9 @@ class ModelTaskController( } fun bindUpdatedWidgets(dataModel: BgDataModel) { - val widgets = dataModel.widgetsModel.getWidgetsListForPicker(app.context) + val widgets = + WidgetsListBaseEntriesBuilder(app.context) + .build(dataModel.widgetsModel.widgetsByPackageItem) scheduleCallbackTask { it.bindAllWidgets(widgets) } } diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 079987b0b5..5464afed2e 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -109,7 +109,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { final IconCache iconCache = app.getIconCache(); final String[] packages = mPackages; - final int N = packages.length; + final int packageCount = packages.length; final FlagOp flagOp; final HashSet packageSet = new HashSet<>(Arrays.asList(packages)); final Predicate matcher = mOp == OP_USER_AVAILABILITY_CHANGE @@ -119,11 +119,12 @@ public class PackageUpdatedTask implements ModelUpdateTask { final HashMap> activitiesLists = new HashMap<>(); if (DEBUG) { Log.d(TAG, "Package updated: mOp=" + getOpString() - + " packages=" + Arrays.toString(packages)); + + " packages=" + Arrays.toString(packages) + + ", user=" + mUser); } switch (mOp) { case OP_ADD: { - for (int i = 0; i < N; i++) { + for (int i = 0; i < packageCount; i++) { iconCache.updateIconsForPkg(packages[i], mUser); if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) { if (DEBUG) { @@ -146,7 +147,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { + " Look for earlier AllAppsList logs to find more information."); removedComponents.add(a.componentName); })) { - for (int i = 0; i < N; i++) { + for (int i = 0; i < packageCount; i++) { iconCache.updateIconsForPkg(packages[i], mUser); activitiesLists.put(packages[i], appsList.updatePackage(context, packages[i], mUser)); @@ -156,13 +157,13 @@ public class PackageUpdatedTask implements ModelUpdateTask { flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); break; case OP_REMOVE: { - for (int i = 0; i < N; i++) { + for (int i = 0; i < packageCount; i++) { iconCache.removeIconsForPkg(packages[i], mUser); } // Fall through } case OP_UNAVAILABLE: - for (int i = 0; i < N; i++) { + for (int i = 0; i < packageCount; i++) { if (DEBUG) { Log.d(TAG, getOpString() + ": removing package=" + packages[i]); } @@ -217,44 +218,44 @@ public class PackageUpdatedTask implements ModelUpdateTask { // For system apps, package manager send OP_UPDATE when an app is enabled. final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; synchronized (dataModel) { - dataModel.forAllWorkspaceItemInfos(mUser, si -> { + dataModel.forAllWorkspaceItemInfos(mUser, itemInfo -> { boolean infoUpdated = false; boolean shortcutUpdated = false; - ComponentName cn = si.getTargetComponent(); - if (cn != null && matcher.test(si)) { + ComponentName cn = itemInfo.getTargetComponent(); + if (cn != null && matcher.test(itemInfo)) { String packageName = cn.getPackageName(); - if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { - forceKeepShortcuts.add(si.id); + if (itemInfo.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { + forceKeepShortcuts.add(itemInfo.id); if (mOp == OP_REMOVE) { return; } } - if (si.isPromise() && isNewApkAvailable) { + if (itemInfo.isPromise() && isNewApkAvailable) { boolean isTargetValid = !cn.getClassName().equals( IconCache.EMPTY_CLASS_NAME); - if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { List shortcut = new ShortcutRequest(context, mUser) .forPackage(cn.getPackageName(), - si.getDeepShortcutId()) + itemInfo.getDeepShortcutId()) .query(ShortcutRequest.PINNED); if (shortcut.isEmpty()) { isTargetValid = false; if (DEBUG) { Log.d(TAG, "Pinned Shortcut not found for updated" - + " package=" + si.getTargetPackage()); + + " package=" + itemInfo.getTargetPackage()); } } else { if (DEBUG) { Log.d(TAG, "Found pinned shortcut for updated" - + " package=" + si.getTargetPackage() + + " package=" + itemInfo.getTargetPackage() + ", isTargetValid=" + isTargetValid); } - si.updateFromDeepShortcutInfo(shortcut.get(0), context); + itemInfo.updateFromDeepShortcutInfo(shortcut.get(0), context); infoUpdated = true; } } else if (isTargetValid) { @@ -262,39 +263,39 @@ public class PackageUpdatedTask implements ModelUpdateTask { .isActivityEnabled(cn, mUser); } - if (!isTargetValid && (si.hasStatusFlag( + if (!isTargetValid && (itemInfo.hasStatusFlag( FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON) - || si.isArchived())) { - if (updateWorkspaceItemIntent(context, si, packageName)) { + || itemInfo.isArchived())) { + if (updateWorkspaceItemIntent(context, itemInfo, packageName)) { infoUpdated = true; - } else if (si.hasPromiseIconUi()) { - removedShortcuts.add(si.id); + } else if (itemInfo.hasPromiseIconUi()) { + removedShortcuts.add(itemInfo.id); if (DEBUG) { FileLog.w(TAG, "Removing restored shortcut promise icon" + " that no longer points to valid component." - + " id=" + si.id - + ", package=" + si.getTargetPackage() - + ", status=" + si.status - + ", isArchived=" + si.isArchived()); + + " id=" + itemInfo.id + + ", package=" + itemInfo.getTargetPackage() + + ", status=" + itemInfo.status + + ", isArchived=" + itemInfo.isArchived()); } return; } } else if (!isTargetValid) { - removedShortcuts.add(si.id); + removedShortcuts.add(itemInfo.id); if (DEBUG) { FileLog.w(TAG, "Removing shortcut that no longer points to" + " valid component." - + " id=" + si.id - + " package=" + si.getTargetPackage() - + " status=" + si.status); + + " id=" + itemInfo.id + + " package=" + itemInfo.getTargetPackage() + + " status=" + itemInfo.status); } return; } else { - si.status = WorkspaceItemInfo.DEFAULT; + itemInfo.status = WorkspaceItemInfo.DEFAULT; infoUpdated = true; } } else if (isNewApkAvailable && removedComponents.contains(cn)) { - if (updateWorkspaceItemIntent(context, si, packageName)) { + if (updateWorkspaceItemIntent(context, itemInfo, packageName)) { infoUpdated = true; } } @@ -304,7 +305,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { packageName); // TODO: See if we can migrate this to // AppInfo#updateRuntimeFlagsForActivityTarget - si.setProgressLevel( + itemInfo.setProgressLevel( activities == null || activities.isEmpty() ? 100 : PackageManagerHelper.getLoadingProgress( @@ -313,42 +314,42 @@ public class PackageUpdatedTask implements ModelUpdateTask { // In case an app is archived, we need to make sure that archived state // in WorkspaceItemInfo is refreshed. if (Flags.enableSupportForArchiving() && !activities.isEmpty()) { - boolean newArchivalState = activities.get( - 0).getActivityInfo().isArchived; - if (newArchivalState != si.isArchived()) { - si.runtimeStatusFlags ^= FLAG_ARCHIVED; + boolean newArchivalState = activities.get(0) + .getActivityInfo().isArchived; + if (newArchivalState != itemInfo.isArchived()) { + itemInfo.runtimeStatusFlags ^= FLAG_ARCHIVED; infoUpdated = true; } } - if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) { + if (itemInfo.itemType == Favorites.ITEM_TYPE_APPLICATION) { if (activities != null && !activities.isEmpty()) { - si.setNonResizeable(ApiWrapper.INSTANCE.get(context) + itemInfo.setNonResizeable(ApiWrapper.INSTANCE.get(context) .isNonResizeableActivity(activities.get(0))); } - iconCache.getTitleAndIcon(si, si.usingLowResIcon()); + iconCache.getTitleAndIcon(itemInfo, itemInfo.usingLowResIcon()); infoUpdated = true; } } - int oldRuntimeFlags = si.runtimeStatusFlags; - si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags); - if (si.runtimeStatusFlags != oldRuntimeFlags) { + int oldRuntimeFlags = itemInfo.runtimeStatusFlags; + itemInfo.runtimeStatusFlags = flagOp.apply(itemInfo.runtimeStatusFlags); + if (itemInfo.runtimeStatusFlags != oldRuntimeFlags) { shortcutUpdated = true; } } if (infoUpdated || shortcutUpdated) { - updatedWorkspaceItems.add(si); + updatedWorkspaceItems.add(itemInfo); } - if (infoUpdated && si.id != ItemInfo.NO_ID) { - taskController.getModelWriter().updateItemInDatabase(si); + if (infoUpdated && itemInfo.id != ItemInfo.NO_ID) { + taskController.getModelWriter().updateItemInDatabase(itemInfo); } }); for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) { if (mUser.equals(widgetInfo.user) && widgetInfo.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) + LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) && packageSet.contains(widgetInfo.providerName.getPackageName())) { widgetInfo.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY @@ -391,7 +392,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { } else if (mOp == OP_UPDATE) { // Mark disabled packages in the broadcast to be removed final LauncherApps launcherApps = context.getSystemService(LauncherApps.class); - for (int i=0; i> mWidgetsList = new HashMap<>(); + private final Map> mWidgetsByPackageItem = new HashMap<>(); /** - * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All - * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall - * list of {@link WidgetsListBaseEntry}s is not sorted. - * - * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) + * Returns all widgets keyed by their component key. */ - public synchronized ArrayList getFilteredWidgetsListForPicker( - Context context, - Predicate widgetItemFilter) { - if (!WIDGETS_ENABLED) { - return new ArrayList<>(); - } - ArrayList result = new ArrayList<>(); - AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context); - - for (Map.Entry> entry : mWidgetsList.entrySet()) { - PackageItemInfo pkgItem = entry.getKey(); - List widgetItems = entry.getValue() - .stream() - .filter(widgetItemFilter).toList(); - if (!widgetItems.isEmpty()) { - String sectionName = (pkgItem.title == null) ? "" : - indexer.computeSectionName(pkgItem.title); - result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems)); - result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems)); - } - } - return result; - } - - /** - * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row - * are sorted (based on label and user), but the overall list of - * {@link WidgetsListBaseEntry}s is not sorted. - * - * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) - */ - public synchronized ArrayList getWidgetsListForPicker(Context context) { - // return all items - return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true); - } - - /** Returns a mapping of packages to their widgets without static shortcuts. */ - public synchronized Map> getAllWidgetsWithoutShortcuts() { + public synchronized Map getWidgetsByComponentKey() { if (!WIDGETS_ENABLED) { return Collections.emptyMap(); } - Map> packagesToWidgets = new HashMap<>(); - mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> { - List widgets = widgetsAndShortcuts.stream() - .filter(item -> item.widgetInfo != null) - .collect(toList()); - if (widgets.size() > 0) { - packagesToWidgets.put( - new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user), - widgets); - } - }); - return packagesToWidgets; + return mWidgetsByPackageItem.values().stream() + .flatMap(Collection::stream).distinct() + .collect(Collectors.toMap( + widget -> new ComponentKey(widget.componentName, widget.user), + Function.identity() + )); } /** - * Returns a map of widget component keys to corresponding widget items. Excludes the - * shortcuts. + * Returns widgets grouped by the package item that they should belong to. */ - public synchronized Map getAllWidgetComponentsWithoutShortcuts() { + public synchronized Map> getWidgetsByPackageItem() { if (!WIDGETS_ENABLED) { return Collections.emptyMap(); } - Map widgetsMap = new HashMap<>(); - mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> - widgetsAndShortcuts.stream().filter(item -> item.widgetInfo != null).forEach( - item -> widgetsMap.put(new ComponentKey(item.componentName, item.user), - item))); - return widgetsMap; + return new HashMap<>(mWidgetsByPackageItem); } /** @@ -210,14 +155,14 @@ public class WidgetsModel { if (packageUser == null) { // Clear the list if this is an update on all widgets and shortcuts. - mWidgetsList.clear(); + mWidgetsByPackageItem.clear(); } else { // Otherwise, only clear the widgets and shortcuts for the changed package. - mWidgetsList.remove(packageItemInfoCache.getOrCreate(packageUser)); + mWidgetsByPackageItem.remove(packageItemInfoCache.getOrCreate(packageUser)); } // add and update. - mWidgetsList.putAll(rawWidgetsShortcuts.stream() + mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream() .filter(new WidgetValidityCheck(app)) .filter(new WidgetFlagCheck()) .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream() @@ -237,7 +182,7 @@ public class WidgetsModel { return; } WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext()); - for (Entry> entry : mWidgetsList.entrySet()) { + for (Entry> entry : mWidgetsByPackageItem.entrySet()) { if (packageNames.contains(entry.getKey().packageName)) { List items = entry.getValue(); int count = items.size(); @@ -258,50 +203,6 @@ public class WidgetsModel { } } - private PackageItemInfo createPackageItemInfo( - ComponentName providerName, - UserHandle user, - int category - ) { - if (category == NO_CATEGORY) { - return new PackageItemInfo(providerName.getPackageName(), user); - } else { - return new PackageItemInfo("" , category, user); - } - } - - private IntSet getCategories(ComponentName providerName, Context context) { - IntSet categories = WidgetSections.getWidgetsToCategory(context).get(providerName); - if (categories != null) { - return categories; - } - categories = new IntSet(); - categories.add(NO_CATEGORY); - return categories; - } - - public WidgetItem getWidgetProviderInfoByProviderName( - ComponentName providerName, UserHandle user, Context context) { - if (!WIDGETS_ENABLED) { - return null; - } - IntSet categories = getCategories(providerName, context); - - // Checking if we have a provider in any of the categories. - for (Integer category: categories) { - PackageItemInfo key = createPackageItemInfo(providerName, user, category); - List widgets = mWidgetsList.get(key); - if (widgets != null) { - return widgets.stream().filter( - item -> item.componentName.equals(providerName) - ) - .findFirst() - .orElse(null); - } - } - return null; - } - /** Returns {@link PackageItemInfo} of a pending widget. */ public static PackageItemInfo newPendingItemInfo(Context context, ComponentName provider, UserHandle user) { diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt index 90e47d66cc..1f1e514ee3 100644 --- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -30,7 +30,6 @@ import com.android.launcher3.Flags import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherSettings.Favorites -import com.android.launcher3.Utilities import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError import com.android.launcher3.config.FeatureFlags import com.android.launcher3.logging.FileLog @@ -76,7 +75,7 @@ class WorkspaceItemProcessor( private val pmHelper: PackageManagerHelper, private val iconRequestInfos: MutableList>, private val unlockedUsers: LongSparseArray, - private val allDeepShortcuts: MutableList + private val allDeepShortcuts: MutableList, ) { private val isSafeMode = app.isSafeModeEnabled @@ -97,7 +96,7 @@ class WorkspaceItemProcessor( // User has been deleted, remove the item. c.markDeleted( "User has been deleted for item id=${c.id}", - RestoreError.PROFILE_DELETED + RestoreError.PROFILE_DELETED, ) return } @@ -168,7 +167,7 @@ class WorkspaceItemProcessor( FileLog.d( TAG, "Activity not enabled for id=${c.id}, component=$cn, user=${c.user}." + - " Will attempt to find fallback Activity for targetPkg=$targetPkg." + " Will attempt to find fallback Activity for targetPkg=$targetPkg.", ) intent = pmHelper.getAppLaunchIntent(targetPkg, c.user) if (intent != null) { @@ -178,7 +177,7 @@ class WorkspaceItemProcessor( c.markDeleted( "No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." + " Unable to create launch Intent.", - RestoreError.MISSING_INFO + RestoreError.MISSING_INFO, ) return } @@ -213,7 +212,7 @@ class WorkspaceItemProcessor( else -> { c.markDeleted( "removing app that is not restored and not installing. package: $targetPkg", - RestoreError.APP_NOT_INSTALLED + RestoreError.APP_NOT_INSTALLED, ) return } @@ -238,7 +237,7 @@ class WorkspaceItemProcessor( // Do not wait for external media load anymore. c.markDeleted( "Invalid package removed: $targetPkg", - RestoreError.APP_NOT_INSTALLED + RestoreError.APP_NOT_INSTALLED, ) return } @@ -270,7 +269,7 @@ class WorkspaceItemProcessor( // The shortcut is no longer valid. c.markDeleted( "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}", - RestoreError.SHORTCUT_NOT_FOUND + RestoreError.SHORTCUT_NOT_FOUND, ) return } @@ -337,7 +336,7 @@ class WorkspaceItemProcessor( activityInfo, userCache.getUserInfo(c.user), ApiWrapper.INSTANCE[app.context], - pmHelper + pmHelper, ) } if ( @@ -445,7 +444,7 @@ class WorkspaceItemProcessor( ", id=${c.id}," + ", appWidgetId=${c.appWidgetId}," + ", component=${component}", - RestoreError.INVALID_LOCATION + RestoreError.INVALID_LOCATION, ) return } @@ -456,7 +455,7 @@ class WorkspaceItemProcessor( ", appWidgetId=${c.appWidgetId}," + ", component=${component}," + ", container=${c.container}", - RestoreError.INVALID_LOCATION + RestoreError.INVALID_LOCATION, ) return } @@ -470,7 +469,7 @@ class WorkspaceItemProcessor( TAG, "processWidget: id=${c.id}" + ", appWidgetId=${c.appWidgetId}" + - ", inflationResult=$inflationResult" + ", inflationResult=$inflationResult", ) when (inflationResult.type) { WidgetInflater.TYPE_DELETE -> { @@ -496,7 +495,7 @@ class WorkspaceItemProcessor( ", appWidgetId=${c.appWidgetId}" + ", component=${component}" + ", restoreFlag:=${c.restoreFlag}", - RestoreError.APP_NOT_INSTALLED + RestoreError.APP_NOT_INSTALLED, ) return } else if ( @@ -512,7 +511,7 @@ class WorkspaceItemProcessor( WidgetsModel.newPendingItemInfo( app.context, appWidgetInfo.providerName, - appWidgetInfo.user + appWidgetInfo.user, ) iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false) } @@ -522,7 +521,7 @@ class WorkspaceItemProcessor( lapi, app.context, appWidgetInfo.spanX, - appWidgetInfo.spanY + appWidgetInfo.spanY, ) } @@ -541,7 +540,7 @@ class WorkspaceItemProcessor( " processWidget: Widget ${lapi.component} minSizes not met: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}," + " id: ${c.id}," + " appWidgetId: ${c.appWidgetId}," + - " component=${component}" + " component=${component}", ) logWidgetInfo(app.invariantDeviceProfile, lapi) } @@ -554,7 +553,7 @@ class WorkspaceItemProcessor( private fun logWidgetInfo( idp: InvariantDeviceProfile, - widgetProviderInfo: LauncherAppWidgetProviderInfo + widgetProviderInfo: LauncherAppWidgetProviderInfo, ) { val cellSize = Point() for (deviceProfile in idp.supportedProfiles) { @@ -565,7 +564,7 @@ class WorkspaceItemProcessor( " available height: ${deviceProfile.availableHeightPx}," + " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," + " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," + - " cellSize: $cellSize" + " cellSize: $cellSize", ) } val widgetDimension = StringBuilder() @@ -583,21 +582,19 @@ class WorkspaceItemProcessor( .append("defaultHeight: ") .append(widgetProviderInfo.minHeight) .append("\n") - if (Utilities.ATLEAST_S) { - widgetDimension - .append("targetCellWidth: ") - .append(widgetProviderInfo.targetCellWidth) - .append("\n") - .append("targetCellHeight: ") - .append(widgetProviderInfo.targetCellHeight) - .append("\n") - .append("maxResizeWidth: ") - .append(widgetProviderInfo.maxResizeWidth) - .append("\n") - .append("maxResizeHeight: ") - .append(widgetProviderInfo.maxResizeHeight) - .append("\n") - } + widgetDimension + .append("targetCellWidth: ") + .append(widgetProviderInfo.targetCellWidth) + .append("\n") + .append("targetCellHeight: ") + .append(widgetProviderInfo.targetCellHeight) + .append("\n") + .append("maxResizeWidth: ") + .append(widgetProviderInfo.maxResizeWidth) + .append("\n") + .append("maxResizeHeight: ") + .append(widgetProviderInfo.maxResizeHeight) + .append("\n") FileLog.d(TAG, widgetDimension.toString()) } } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index b82d0a0985..b706d249f2 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -51,6 +51,7 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Workspace; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logger.LauncherAtom.AllAppsContainer; +import com.android.launcher3.logger.LauncherAtom.Attribute; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; import com.android.launcher3.logger.LauncherAtom.PredictionContainer; import com.android.launcher3.logger.LauncherAtom.SettingsContainer; @@ -67,6 +68,9 @@ import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.UserIconInfo; import com.android.systemui.shared.system.SysUiStatsLog; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; /** @@ -77,8 +81,6 @@ public class ItemInfo { public static final boolean DEBUG = false; public static final int NO_ID = -1; - // An id that doesn't match any item, including predicted apps with have an id=NO_ID - public static final int NO_MATCHING_ID = Integer.MIN_VALUE; /** Hidden field Settings.Secure.NAV_BAR_KIDS_MODE */ private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor("nav_bar_kids_mode"); @@ -187,6 +189,12 @@ public class ItemInfo { @NonNull public UserHandle user; + @NonNull + private ExtendedContainers mExtendedContainers = ExtendedContainers.getDefaultInstance(); + + @NonNull + private List mAttributeList = Collections.EMPTY_LIST; + public ItemInfo() { user = Process.myUserHandle(); } @@ -433,6 +441,7 @@ public class ItemInfo { UserCache.INSTANCE.executeIfCreated(cache -> itemBuilder.setUserType(getUserType(cache.getUserInfo(user)))); itemBuilder.setRank(rank); + itemBuilder.addAllItemAttributes(mAttributeList); return itemBuilder; } @@ -491,7 +500,7 @@ public class ItemInfo { default: if (container <= EXTENDED_CONTAINERS) { return ContainerInfo.newBuilder() - .setExtendedContainers(getExtendedContainer()) + .setExtendedContainers(mExtendedContainers) .build(); } } @@ -499,12 +508,21 @@ public class ItemInfo { } /** - * Returns non-AOSP container wrapped by {@link ExtendedContainers} object. Should be overridden - * by build variants. + * Sets extra container info wrapped by {@link ExtendedContainers} object. */ - @NonNull - protected ExtendedContainers getExtendedContainer() { - return ExtendedContainers.getDefaultInstance(); + public void setExtendedContainers(@NonNull ExtendedContainers extendedContainers) { + mExtendedContainers = extendedContainers; + } + + /** + * Adds extra attributes to be added during logs + */ + public void addLogAttributes(List attributeList) { + if (mAttributeList.isEmpty()) { + mAttributeList = new ArrayList<>(attributeList); + } else { + mAttributeList.addAll(attributeList); + } } /** @@ -525,6 +543,14 @@ public class ItemInfo { this.title = title; } + /** + * Returns a string ID that is stable for a user session, but may not be persisted + */ + @Nullable + public Object getStableId() { + return getComponentKey(); + } + private int getUserType(UserIconInfo info) { if (info == null) { return SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_UNKNOWN; diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java index f4dda5593a..361f09d418 100644 --- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java @@ -21,7 +21,6 @@ import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_ import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY; -import static com.android.launcher3.Utilities.ATLEAST_S; import android.appwidget.AppWidgetHostView; import android.content.ComponentName; @@ -233,16 +232,16 @@ public class LauncherAppWidgetInfo extends ItemInfo { if (providerInfo.isConfigurationOptional()) { widgetFeatures |= FEATURE_OPTIONAL_CONFIGURATION; } - if (ATLEAST_S && providerInfo.previewLayout != Resources.ID_NULL) { + if (providerInfo.previewLayout != Resources.ID_NULL) { widgetFeatures |= FEATURE_PREVIEW_LAYOUT; } - if (ATLEAST_S && providerInfo.targetCellWidth > 0 || providerInfo.targetCellHeight > 0) { + if (providerInfo.targetCellWidth > 0 || providerInfo.targetCellHeight > 0) { widgetFeatures |= FEATURE_TARGET_CELL_SIZE; } if (providerInfo.minResizeWidth > 0 || providerInfo.minResizeHeight > 0) { widgetFeatures |= FEATURE_MIN_SIZE; } - if (ATLEAST_S && providerInfo.maxResizeWidth > 0 || providerInfo.maxResizeHeight > 0) { + if (providerInfo.maxResizeWidth > 0 || providerInfo.maxResizeHeight > 0) { widgetFeatures |= FEATURE_MAX_SIZE; } if (hostView instanceof LauncherAppWidgetHostView && diff --git a/src/com/android/launcher3/model/data/TaskItemInfo.kt b/src/com/android/launcher3/model/data/TaskItemInfo.kt new file mode 100644 index 0000000000..fc1cd4d418 --- /dev/null +++ b/src/com/android/launcher3/model/data/TaskItemInfo.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.launcher3.model.data + +/** + * Temporary class holding a Task ID to allow us to reference a Task when clicking a hotseat item. + * + * TODO(b/315344726): Remove this class when we have proper Taskbar support for multi-instance apps + */ +class TaskItemInfo(val taskId: Int, itemInfo: WorkspaceItemInfo) : WorkspaceItemInfo(itemInfo) diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java index 40e3813fe3..f31bf1e5fe 100644 --- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java +++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java @@ -25,6 +25,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.launcher3.Flags; import com.android.launcher3.LauncherSettings; @@ -97,6 +98,8 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { public int options; + @Nullable + private ShortcutInfo mShortcutInfo = null; public WorkspaceItemInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; @@ -175,6 +178,9 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { public void updateFromDeepShortcutInfo(@NonNull final ShortcutInfo shortcutInfo, @NonNull final Context context) { + if (com.android.wm.shell.Flags.enableBubbleAnything()) { + mShortcutInfo = shortcutInfo; + } // {@link ShortcutInfo#getActivity} can change during an update. Recreate the intent intent = ShortcutKey.makeIntent(shortcutInfo); title = shortcutInfo.getShortLabel(); @@ -204,6 +210,11 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new); } + @Nullable + public ShortcutInfo getDeepShortcutInfo() { + return mShortcutInfo; + } + /** * {@code true} if the shortcut is disabled due to its app being a lower version. */ diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java index e44ea1d69d..a691e45097 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java +++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java @@ -43,6 +43,7 @@ import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.Insettable; import com.android.launcher3.R; @@ -131,7 +132,8 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator private float mCurrentPosition; private float mFinalPosition; private boolean mIsScrollPaused; - private boolean mIsTwoPanels; + @VisibleForTesting + boolean mIsTwoPanels; private ObjectAnimator mAnimator; private @Nullable ObjectAnimator mAlphaAnimator; @@ -477,6 +479,21 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator return sTempRect; } + @VisibleForTesting + int getActivePage() { + return mActivePage; + } + + @VisibleForTesting + int getNumPages() { + return mNumPages; + } + + @VisibleForTesting + float getCurrentPosition() { + return mCurrentPosition; + } + private class MyOutlineProver extends ViewOutlineProvider { @Override diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java deleted file mode 100644 index bde4e525a1..0000000000 --- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.android.launcher3.pageindicators; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.util.Property; -import android.view.View; -import android.view.ViewConfiguration; - -import com.android.launcher3.Insettable; -import com.android.launcher3.Launcher; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.Themes; - -/** - * A PageIndicator that briefly shows a fraction of a line when moving between pages - * - * The fraction is 1 / number of pages and the position is based on the progress of the page scroll. - */ -public class WorkspacePageIndicator extends View implements Insettable, PageIndicator { - - private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration(); - private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay(); - public static final int WHITE_ALPHA = (int) (0.70f * 255); - public static final int BLACK_ALPHA = (int) (0.65f * 255); - - private static final int LINE_ALPHA_ANIMATOR_INDEX = 0; - private static final int NUM_PAGES_ANIMATOR_INDEX = 1; - private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2; - private static final int ANIMATOR_COUNT = 3; - - private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT]; - - private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper()); - private final Launcher mLauncher; - - private boolean mShouldAutoHide = true; - - // The alpha of the line when it is showing. - private int mActiveAlpha = 0; - // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha). - private int mToAlpha; - // A float value representing the number of pages, to allow for an animation when it changes. - private float mNumPagesFloat; - private int mCurrentScroll; - private int mTotalScroll; - private Paint mLinePaint; - private final int mLineHeight; - - private static final Property PAINT_ALPHA - = new Property(Integer.class, "paint_alpha") { - @Override - public Integer get(WorkspacePageIndicator obj) { - return obj.mLinePaint.getAlpha(); - } - - @Override - public void set(WorkspacePageIndicator obj, Integer alpha) { - obj.mLinePaint.setAlpha(alpha); - obj.invalidate(); - } - }; - - private static final Property NUM_PAGES - = new Property(Float.class, "num_pages") { - @Override - public Float get(WorkspacePageIndicator obj) { - return obj.mNumPagesFloat; - } - - @Override - public void set(WorkspacePageIndicator obj, Float numPages) { - obj.mNumPagesFloat = numPages; - obj.invalidate(); - } - }; - - private static final Property TOTAL_SCROLL - = new Property(Integer.class, "total_scroll") { - @Override - public Integer get(WorkspacePageIndicator obj) { - return obj.mTotalScroll; - } - - @Override - public void set(WorkspacePageIndicator obj, Integer totalScroll) { - obj.mTotalScroll = totalScroll; - obj.invalidate(); - } - }; - - private Runnable mHideLineRunnable = () -> animateLineToAlpha(0); - - public WorkspacePageIndicator(Context context) { - this(context, null); - } - - public WorkspacePageIndicator(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - Resources res = context.getResources(); - mLinePaint = new Paint(); - mLinePaint.setAlpha(0); - - mLauncher = Launcher.getLauncher(context); - mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height); - - boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText); - mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA; - mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE); - } - - @Override - protected void onDraw(Canvas canvas) { - if (mTotalScroll == 0 || mNumPagesFloat == 0) { - return; - } - - // Compute and draw line rect. - float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f); - int availableWidth = getWidth(); - int lineWidth = (int) (availableWidth / mNumPagesFloat); - int lineLeft = (int) (progress * (availableWidth - lineWidth)); - int lineRight = lineLeft + lineWidth; - - canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight, - getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint); - } - - @Override - public void setScroll(int currentScroll, int totalScroll) { - if (getAlpha() == 0) { - return; - } - animateLineToAlpha(mActiveAlpha); - - mCurrentScroll = currentScroll; - if (mTotalScroll == 0) { - mTotalScroll = totalScroll; - } else if (mTotalScroll != totalScroll) { - animateToTotalScroll(totalScroll); - } else { - invalidate(); - } - - if (mShouldAutoHide) { - hideAfterDelay(); - } - } - - private void hideAfterDelay() { - mDelayedLineFadeHandler.removeCallbacksAndMessages(null); - mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY); - } - - @Override - public void setActiveMarker(int activePage) { } - - @Override - public void setMarkersCount(int numMarkers) { - if (Float.compare(numMarkers, mNumPagesFloat) != 0) { - setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers), - NUM_PAGES_ANIMATOR_INDEX); - } else { - if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) { - mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel(); - mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null; - } - } - } - - @Override - public void setShouldAutoHide(boolean shouldAutoHide) { - mShouldAutoHide = shouldAutoHide; - if (shouldAutoHide && mLinePaint.getAlpha() > 0) { - hideAfterDelay(); - } else if (!shouldAutoHide) { - mDelayedLineFadeHandler.removeCallbacksAndMessages(null); - } - } - - private void animateLineToAlpha(int alpha) { - if (alpha == mToAlpha) { - // Ignore the new animation if it is going to the same alpha as the current animation. - return; - } - mToAlpha = alpha; - setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha), - LINE_ALPHA_ANIMATOR_INDEX); - } - - private void animateToTotalScroll(int totalScroll) { - setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll), - TOTAL_SCROLL_ANIMATOR_INDEX); - } - - /** - * Starts the given animator and stores it in the provided index in {@link #mAnimators} until - * the animation ends. - * - * If an animator is already at the index (i.e. it is already playing), it is canceled and - * replaced with the new animator. - */ - private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) { - if (mAnimators[animatorIndex] != null) { - mAnimators[animatorIndex].cancel(); - } - mAnimators[animatorIndex] = animator; - mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimators[animatorIndex] = null; - } - }); - mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION); - mAnimators[animatorIndex].start(); - } - - /** - * Pauses all currently running animations. - */ - @Override - public void pauseAnimations() { - for (int i = 0; i < ANIMATOR_COUNT; i++) { - if (mAnimators[i] != null) { - mAnimators[i].pause(); - } - } - } - - /** - * Force-ends all currently running or paused animations. - */ - @Override - public void skipAnimationsToEnd() { - for (int i = 0; i < ANIMATOR_COUNT; i++) { - if (mAnimators[i] != null) { - mAnimators[i].end(); - } - } - } - - /** - * We need to override setInsets to prevent InsettableFrameLayout from applying different - * margins on the page indicator. - */ - @Override - public void setInsets(Rect insets) { - } -} diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java index 24d58f3e98..856c294a3a 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -25,6 +25,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.os.Build; import android.os.UserHandle; +import android.util.Log; import android.util.SparseArray; import androidx.annotation.NonNull; @@ -32,7 +33,7 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.Flags; -import com.android.launcher3.Utilities; +import com.android.launcher3.logging.FileLog; import com.android.launcher3.util.PackageUserKey; import java.lang.ref.WeakReference; @@ -42,6 +43,8 @@ import java.util.Objects; @WorkerThread public class InstallSessionTracker extends PackageInstaller.SessionCallback { + public static final String TAG = "InstallSessionTracker"; + // Lazily initialized private SparseArray mActiveSessions = null; @@ -76,6 +79,11 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { } SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback); if (sessionInfo != null) { + FileLog.d(TAG, "onCreated: Install session created for" + + " appPackageName=" + sessionInfo.getAppPackageName() + + ", sessionId=" + sessionInfo.getSessionId() + + ", appIcon=" + sessionInfo.getAppIcon() + + ", appLabel=" + sessionInfo.getAppLabel()); callback.onInstallSessionCreated(PackageInstallInfo.fromInstallingState(sessionInfo)); } @@ -103,6 +111,10 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { activeSessions.remove(sessionId); if (key != null && key.mPackageName != null) { + FileLog.d(TAG, "onFinished: active install session finished for" + + " appPackageName=" + key.mPackageName + + ", sessionId=" + sessionId + + ", success=" + success); String packageName = key.mPackageName; PackageInstallInfo info = PackageInstallInfo.fromState( success ? STATUS_INSTALLED : STATUS_FAILED, @@ -142,6 +154,11 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { } SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback); if (sessionInfo != null) { + Log.d(TAG, "onBadgingChanged: badging info changed for" + + " appPackageName=" + sessionInfo.getAppPackageName() + + ", sessionId=" + sessionInfo.getSessionId() + + ", appIcon=" + sessionInfo.getAppIcon() + + ", appLabel=" + sessionInfo.getAppLabel()); helper.tryQueuePromiseAppIcon(sessionInfo); } } diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java index 667136ae00..47afeef703 100644 --- a/src/com/android/launcher3/pm/PinRequestHelper.java +++ b/src/com/android/launcher3/pm/PinRequestHelper.java @@ -77,8 +77,9 @@ public class PinRequestHelper { WorkspaceItemInfo info = new WorkspaceItemInfo(si, context); // Apply the unbadged icon synchronously using the caching logic directly and // fetch the actual icon asynchronously. - info.bitmap = new ShortcutCachingLogic().loadIcon(context, si); - LauncherAppState.getInstance(context).getModel().updateAndBindWorkspaceItem(info, si); + LauncherAppState app = LauncherAppState.getInstance(context); + info.bitmap = new ShortcutCachingLogic().loadIcon(context, app.getIconCache(), si); + app.getModel().updateAndBindWorkspaceItem(info, si); return info; } else { return null; diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java index ed25186da4..e8619610a8 100644 --- a/src/com/android/launcher3/pm/UserCache.java +++ b/src/com/android/launcher3/pm/UserCache.java @@ -75,7 +75,7 @@ public class UserCache implements SafeCloseable { private final List> mUserEventListeners = new ArrayList<>(); private final SimpleBroadcastReceiver mUserChangeReceiver = - new SimpleBroadcastReceiver(this::onUsersChanged); + new SimpleBroadcastReceiver(MODEL_EXECUTOR, this::onUsersChanged); private final Context mContext; @@ -183,6 +183,11 @@ public class UserCache implements SafeCloseable { mUserToSerialMap.put(userHandle, info); } + @VisibleForTesting + public void putToPreInstallCache(UserHandle userHandle, List preInstalledApps) { + mUserToPreInstallAppMap.put(userHandle, preInstalledApps); + } + /** * @see UserManager#getUserProfiles() */ diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index 4d4a8f749a..c2debfa2ed 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -16,8 +16,6 @@ package com.android.launcher3.popup; -import static androidx.core.content.ContextCompat.getColorStateList; - import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.app.animation.Interpolators.LINEAR; @@ -56,8 +54,6 @@ import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; -import java.util.Arrays; - /** * A container for shortcuts to deep links and notifications associated with an app. * @@ -130,7 +126,7 @@ public abstract class ArrowPopup // Tag for Views that have children that will need to be iterated to add styling. private final String mIterateChildrenTag; - protected final int[] mColorIds; + protected final int[] mColors; public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); @@ -142,8 +138,7 @@ public abstract class ArrowPopup // Initialize arrow view final Resources resources = getResources(); - mArrowColor = getColorStateList(getContext(), R.color.popup_color_background) - .getDefaultColor(); + mArrowColor = Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceContainer); mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin); mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); @@ -158,21 +153,25 @@ public abstract class ArrowPopup mRoundedTop = new GradientDrawable(); int popupPrimaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary); mRoundedTop.setColor(popupPrimaryColor); - mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius, + mRoundedTop.setCornerRadii(new float[]{mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius}); mRoundedBottom = new GradientDrawable(); mRoundedBottom.setColor(popupPrimaryColor); - mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius, + mRoundedBottom.setCornerRadii(new float[]{smallerRadius, smallerRadius, smallerRadius, smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius}); mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children); if (mActivityContext.canUseMultipleShadesForPopup()) { - mColorIds = new int[]{R.color.popup_shade_first, R.color.popup_shade_second, - R.color.popup_shade_third}; + mColors = new int[]{ + getContext().getColor(R.color.popup_shade_first), + getContext().getColor(R.color.popup_shade_second), + getContext().getColor(R.color.popup_shade_third) + }; } else { - mColorIds = new int[]{R.color.popup_color_background}; + mColors = new int[]{Themes.getAttrColor(getContext(), + R.attr.materialColorSurfaceContainer)}; } } @@ -219,15 +218,14 @@ public abstract class ArrowPopup } /** - * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColorIds}. + * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColors}. * Otherwise, we will use this color for all child views. */ protected void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) { int[] colors = null; if (backgroundColor == Color.TRANSPARENT) { // Lazily get the colors so they match the current wallpaper colors. - colors = Arrays.stream(mColorIds).map( - r -> getColorStateList(getContext(), r).getDefaultColor()).toArray(); + colors = mColors; } int count = viewGroup.getChildCount(); diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index fb463f7d24..8a5e388c50 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -24,28 +24,20 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.dot.DotInfo; -import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.notification.NotificationKeyData; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.ShortcutUtil; -import com.android.launcher3.widget.PendingAddWidgetInfo; -import com.android.launcher3.widget.model.WidgetsListBaseEntry; -import com.android.launcher3.widget.model.WidgetsListContentEntry; -import com.android.launcher3.widget.picker.WidgetRecommendationCategory; import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; /** * Provides data for the popup menu that appears after long-clicking on apps. @@ -62,13 +54,6 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan /** Maps packages to their DotInfo's . */ private Map mPackageUserToDotInfos = new HashMap<>(); - /** All installed widgets. */ - private List mAllWidgets = List.of(); - /** Widgets that can be recommended to the users. */ - private List mRecommendedWidgets = List.of(); - - private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE; - public PopupDataProvider(Consumer> notificationDotsChangeListener) { mNotificationDotsChangeListener = notificationDotsChangeListener; } @@ -183,102 +168,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan })) ? dotInfo : null; } - /** - * Sets a list of recommended widgets ordered by their order of appearance in the widgets - * recommendation UI. - */ - public void setRecommendedWidgets(List recommendedWidgets) { - mRecommendedWidgets = recommendedWidgets; - mChangeListener.onRecommendedWidgetsBound(); - } - - public void setAllWidgets(List allWidgets) { - mAllWidgets = allWidgets; - mChangeListener.onWidgetsBound(); - } - - public void setChangeListener(PopupDataChangeListener listener) { - mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener; - } - - public List getAllWidgets() { - return mAllWidgets; - } - - /** Returns a list of recommended widgets. */ - public List getRecommendedWidgets() { - HashMap allWidgetItems = new HashMap<>(); - mAllWidgets.stream() - .filter(entry -> entry instanceof WidgetsListContentEntry) - .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets - .forEach(widget -> allWidgetItems.put( - new ComponentKey(widget.componentName, widget.user), widget))); - return mRecommendedWidgets.stream() - .map(recommendedWidget -> allWidgetItems.get( - new ComponentKey(recommendedWidget.getTargetComponent(), - recommendedWidget.user))) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** Returns the recommended widgets mapped by their category. */ - @NonNull - public Map> getCategorizedRecommendedWidgets() { - Map allWidgetItems = mAllWidgets.stream() - .filter(entry -> entry instanceof WidgetsListContentEntry) - .flatMap(entry -> entry.mWidgets.stream()) - .distinct() - .collect(Collectors.toMap( - widget -> new ComponentKey(widget.componentName, widget.user), - Function.identity() - )); - return mRecommendedWidgets.stream() - .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo - && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null) - .collect(Collectors.groupingBy( - it -> ((PendingAddWidgetInfo) it).recommendationCategory, - Collectors.collectingAndThen( - Collectors.toList(), - list -> list.stream() - .map(it -> allWidgetItems.get( - new ComponentKey(it.getTargetComponent(), - it.user))) - .filter(Objects::nonNull) - .collect(Collectors.toList()) - ) - )); - } - - public List getWidgetsForPackageUser(PackageUserKey packageUserKey) { - return mAllWidgets.stream() - .filter(row -> row instanceof WidgetsListContentEntry - && row.mPkgItem.packageName.equals(packageUserKey.mPackageName)) - .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream()) - .filter(widget -> packageUserKey.mUser.equals(widget.user)) - .collect(Collectors.toList()); - } - - /** Gets the WidgetsListContentEntry for the currently selected header. */ - public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) { - return (WidgetsListContentEntry) mAllWidgets.stream() - .filter(row -> row instanceof WidgetsListContentEntry - && PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey)) - .findAny() - .orElse(null); - } - public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "PopupDataProvider:"); writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos); } - - public interface PopupDataChangeListener { - - PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { }; - - default void onWidgetsBound() { } - - /** A callback to get notified when recommended widgets are bound. */ - default void onRecommendedWidgetsBound() { } - } } diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java index 4c94f9401f..1fd355772b 100644 --- a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java +++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java @@ -19,6 +19,8 @@ import android.content.Context; import android.view.View; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener; /** * Utility class to handle updates while the popup is visible (like widgets and @@ -27,7 +29,7 @@ import com.android.launcher3.views.ActivityContext; * @param The activity on which the popup shows */ public abstract class PopupLiveUpdateHandler implements - PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener { + WidgetPickerDataChangeListener, View.OnAttachStateChangeListener { protected final T mContext; protected final PopupContainerWithArrow mPopupContainerWithArrow; @@ -40,19 +42,25 @@ public abstract class PopupLiveUpdateHandler extends ItemInfo implements View.OnClickListener { + private static final String TAG = "SystemShortcut"; private final int mIconResId; protected final int mLabelResId; @@ -107,11 +113,12 @@ public abstract class SystemShortcut extends ItemInfo } public static final Factory WIDGETS = (context, itemInfo, originalView) -> { - if (itemInfo.getTargetComponent() == null) return null; - final List widgets = - context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey( - itemInfo.getTargetComponent().getPackageName(), itemInfo.user)); - if (widgets.isEmpty()) { + final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(itemInfo); + if (packageUserKey == null) return null; + + final WidgetPickerData data = context.getWidgetPickerDataProvider().get(); + if (findAllWidgetsForPackageUser(data, packageUserKey).isEmpty()) { + // hides widget picker shortcut if there are no widgets for the package. return null; } return new Widgets(context, itemInfo, originalView); @@ -179,10 +186,12 @@ public abstract class SystemShortcut extends ItemInfo @Override public void onClick(View view) { - dismissTaskMenuView(); Rect sourceBounds = Utilities.getViewBounds(view); + ActivityOptionsWrapper options = mTarget.getActivityLaunchOptions(view, mItemInfo); + // Dismiss the taskMenu when the app launch animation is complete + options.onEndCallback.add(this::dismissTaskMenuView); PackageManagerHelper.startDetailsActivityForInfo(view.getContext(), mItemInfo, - sourceBounds, ActivityOptions.makeBasic().toBundle()); + sourceBounds, options.toBundle()); mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP); } @@ -329,6 +338,14 @@ public abstract class SystemShortcut extends ItemInfo mTarget.getStatsLogManager().logger() .withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP); + if (Flags.enableDismissPredictionUndo()) { + Snackbar.show(mTarget, + view.getContext().getString(R.string.item_removed), R.string.undo, + () -> { }, () -> + mTarget.getStatsLogManager().logger() + .withItemInfo(mItemInfo) + .log(LAUNCHER_DISMISS_PREDICTION_UNDO)); + } } } @@ -382,4 +399,63 @@ public abstract class SystemShortcut extends ItemInfo mAbstractFloatingViewHelper.closeOpenViews(mTarget, true, AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); } + + public static final Factory BUBBLE_SHORTCUT = + (activity, itemInfo, originalView) -> { + if ((itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) + && (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) + && !(itemInfo instanceof WorkspaceItemInfo)) { + return null; + } + return new BubbleShortcut(activity, itemInfo, originalView); + }; + + public interface BubbleActivityStarter { + /** Tell SysUI to show the provided shortcut in a bubble. */ + void showShortcutBubble(ShortcutInfo info); + + /** Tell SysUI to show the provided intent in a bubble. */ + void showAppBubble(Intent intent); + } + + public static class BubbleShortcut extends SystemShortcut { + + private BubbleActivityStarter mStarter; + + public BubbleShortcut(T target, ItemInfo itemInfo, View originalView) { + super(R.drawable.ic_bubble_button, R.string.bubble, target, + itemInfo, originalView); + if (target instanceof BubbleActivityStarter) { + mStarter = (BubbleActivityStarter) target; + } + } + + @Override + public void onClick(View view) { + dismissTaskMenuView(); + if (mStarter == null) { + Log.w(TAG, "starter null!"); + return; + } + // TODO: handle GroupTask (single) items so that recent items in taskbar work + if (mItemInfo instanceof WorkspaceItemInfo) { + WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo; + ShortcutInfo shortcutInfo = workspaceItemInfo.getDeepShortcutInfo(); + if (shortcutInfo != null) { + mStarter.showShortcutBubble(shortcutInfo); + return; + } + } + // If we're here check for an intent + Intent intent = mItemInfo.getIntent(); + if (intent != null) { + if (intent.getPackage() == null) { + intent.setPackage(mItemInfo.getTargetPackage()); + } + mStarter.showAppBubble(intent); + } else { + Log.w(TAG, "unable to bubble, no intent: " + mItemInfo); + } + } + } } diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt index 6d6b3b6497..82229f8f21 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -17,12 +17,13 @@ package com.android.launcher3.recyclerview import android.content.Context +import android.util.Log import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.android.launcher3.BubbleTextView +import com.android.launcher3.BuildConfig import com.android.launcher3.allapps.BaseAllAppsAdapter -import com.android.launcher3.config.FeatureFlags import com.android.launcher3.util.CancellableTask import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR @@ -43,6 +44,12 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { var hasWorkProfile = false private var mCancellableTask: CancellableTask>? = null + companion object { + private const val TAG = "AllAppsRecyclerViewPool" + private const val NULL_LAYOUT_MANAGER_ERROR_STRING = + "activeRv's layoutManager should not be null" + } + /** * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate. */ @@ -54,6 +61,15 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { return } + if (activeRv.layoutManager == null) { + if (BuildConfig.IS_STUDIO_BUILD) { + throw IllegalStateException(NULL_LAYOUT_MANAGER_ERROR_STRING) + } else { + Log.e(TAG, NULL_LAYOUT_MANAGER_ERROR_STRING) + } + return + } + // Create a separate context dedicated for all apps preinflation thread. The goal is to // create a separate AssetManager obj internally to avoid lock contention with // AssetManager obj that is associated with the launcher context on the main thread. @@ -61,7 +77,7 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { ActivityContextDelegate( context.createConfigurationContext(context.resources.configuration), Themes.getActivityThemeRes(context), - context + context, ) // Because we perform onCreateViewHolder() on worker thread, we need a separate @@ -74,9 +90,10 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { context, context.appsView.layoutInflater.cloneInContext(allAppsPreInflationContext), null, - null + null, ) { override fun setAppsPerRow(appsPerRow: Int) = Unit + override fun getLayoutManager(): RecyclerView.LayoutManager? = null } @@ -90,6 +107,11 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { if (task?.canceled == true) { break } + // If activeRv's layout manager has been reset to null on main thread, skip + // the preinflation as we cannot generate correct LayoutParams + if (activeRv.layoutManager == null) { + break + } list.add( adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON) ) @@ -101,7 +123,7 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) { putRecycledView(viewHolders[i]) } - } + }, ) mCancellableTask = task VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask) @@ -121,18 +143,15 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to * suffice fast scrolling. * - * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra - * app icons in size of one all apps pages, so that opening all apps don't need to inflate app - * icons. + * Note that we need to preinfate extra app icons in size of one all apps pages, so that opening + * all apps don't need to inflate app icons. */ fun getPreinflateCount(context: T): Int where T : Context, T : ActivityContext { var targetPreinflateCount = PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns + EXTRA_ICONS_COUNT - if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) { - val grid = ActivityContext.lookupContext(context).deviceProfile - targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns - } + val grid = ActivityContext.lookupContext(context).deviceProfile + targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns if (hasWorkProfile) { targetPreinflateCount *= 2 } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index 0299a23382..9b3292d3ff 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -59,6 +59,7 @@ import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Themes; import com.android.launcher3.views.BaseDragLayer; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; import java.util.HashMap; import java.util.Map; @@ -76,6 +77,7 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity private View mAppsButton; private PopupDataProvider mPopupDataProvider; + private WidgetPickerDataProvider mWidgetPickerDataProvider; private boolean mAppDrawerShown = false; @@ -314,6 +316,11 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity return mPopupDataProvider; } + @Override + public WidgetPickerDataProvider getWidgetPickerDataProvider() { + return mWidgetPickerDataProvider; + } + @Override public OnClickListener getItemOnClickListener() { return this::onIconClicked; diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java index 52ce4e85a6..bd9298b99a 100644 --- a/src/com/android/launcher3/settings/SettingsActivity.java +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -44,11 +44,13 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback; import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback; +import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroup.PreferencePositionCallback; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BuildConfig; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherFiles; import com.android.launcher3.R; import com.android.launcher3.states.RotationHelper; @@ -165,6 +167,7 @@ public class SettingsActivity extends FragmentActivity private boolean mRestartOnResume = false; private String mHighLightKey; + private boolean mPreferenceHighlighted = false; @Override @@ -198,11 +201,62 @@ public class SettingsActivity extends FragmentActivity } } + // If the target preference is not in the current preference screen, find the parent + // preference screen that contains the target preference and set it as the preference + // screen. + if (Flags.navigateToChildPreference() + && mHighLightKey != null + && !isKeyInPreferenceGroup(mHighLightKey, screen)) { + final PreferenceScreen parentPreferenceScreen = + findParentPreference(screen, mHighLightKey); + if (parentPreferenceScreen != null && getActivity() != null) { + if (!TextUtils.isEmpty(parentPreferenceScreen.getTitle())) { + getActivity().setTitle(parentPreferenceScreen.getTitle()); + } + setPreferenceScreen(parentPreferenceScreen); + return; + } + } + if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) { getActivity().setTitle(getPreferenceScreen().getTitle()); } } + private boolean isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent) { + for (int i = 0; i < parent.getPreferenceCount(); i++) { + Preference pref = parent.getPreference(i); + if (pref.getKey() != null && pref.getKey().equals(targetKey)) { + return true; + } + } + return false; + } + + /** + * Finds the parent preference screen for the given target key. + * + * @param parent the parent preference screen + * @param targetKey the key of the preference to find + * @return the parent preference screen that contains the target preference + */ + @Nullable + private PreferenceScreen findParentPreference(PreferenceScreen parent, String targetKey) { + for (int i = 0; i < parent.getPreferenceCount(); i++) { + Preference pref = parent.getPreference(i); + if (pref instanceof PreferenceScreen) { + PreferenceScreen foundKey = findParentPreference((PreferenceScreen) pref, + targetKey); + if (foundKey != null) { + return foundKey; + } + } else if (pref.getKey() != null && pref.getKey().equals(targetKey)) { + return parent; + } + } + return null; + } + @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java index ac07c0f2cf..303290dda3 100644 --- a/src/com/android/launcher3/statemanager/StateManager.java +++ b/src/com/android/launcher3/statemanager/StateManager.java @@ -253,7 +253,7 @@ public class StateManager, if (mConfig.currentAnimation == null) { // Run any queued runnable if (listener != null) { - listener.onAnimationEnd(null); + listener.onAnimationEnd(new AnimatorSet()); } return; } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state) @@ -282,7 +282,7 @@ public class StateManager, // Run any queued runnable if (listener != null) { - listener.onAnimationEnd(null); + listener.onAnimationEnd(new AnimatorSet()); } return; } diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index db2a6e0a8b..6d9b891e53 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -501,7 +501,7 @@ public class TestInformationHandler implements ResourceBasedOverride { /** * Returns the result by getting a generic property on UI thread */ - private static Bundle getUIProperty( + protected static Bundle getUIProperty( BundleSetter bundleSetter, Function provider, Supplier targetSupplier) { return getFromExecutorSync(MAIN_EXECUTOR, () -> { S target = targetSupplier.get(); diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index 3817563b0b..efd1f0d6b4 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -40,11 +40,13 @@ import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.contextualeducation.ContextualEduStatsManager; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.TouchController; +import com.android.systemui.contextualeducation.GestureType; /** * TouchController for handling state changes @@ -388,6 +390,7 @@ public abstract class AbstractStateChangeTouchController } else { logReachedState(mToState); } + updateContextualEduStats(targetState); } protected void goToTargetState(LauncherState targetState) { @@ -403,6 +406,21 @@ public abstract class AbstractStateChangeTouchController .setDuration(0).start(); } + private void updateContextualEduStats(LauncherState targetState) { + if (targetState == NORMAL) { + ContextualEduStatsManager.INSTANCE.get( + mLauncher).updateEduStats(mDetector.isTrackpadGesture(), GestureType.HOME); + } else if (targetState == OVERVIEW) { + ContextualEduStatsManager.INSTANCE.get( + mLauncher).updateEduStats(mDetector.isTrackpadGesture(), GestureType.OVERVIEW); + } else if (targetState == ALL_APPS && !mDetector.isTrackpadGesture()) { + // Only update if it is touch gesture as trackpad gesture is not relevant for all apps + // which only provides keyboard education. + ContextualEduStatsManager.INSTANCE.get( + mLauncher).updateEduStats(/* isTrackpadGesture= */ false, GestureType.ALL_APPS); + } + } + private void logReachedState(LauncherState targetState) { if (mStartState == targetState) { return; diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java index fe4a83b8d0..9dcdf22852 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -22,13 +22,9 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.app.animation.Interpolators.clampToProgress; -import static com.android.app.animation.Interpolators.mapToProgress; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_KEYBOARD_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH; import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE; @@ -37,15 +33,12 @@ import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS; import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE; -import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW; import android.view.MotionEvent; import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.states.StateAnimationConfig; @@ -281,36 +274,6 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { } } - /** - * Applies Animation config values for transition from overview to all apps. - * - * @param threshold progress at which all apps will open upon release - */ - public static void applyOverviewToAllAppsAnimConfig( - DeviceProfile deviceProfile, StateAnimationConfig config, float threshold) { - config.animProps |= StateAnimationConfig.USER_CONTROLLED; - config.animFlags = SKIP_OVERVIEW; - if (deviceProfile.isTablet) { - config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT); - config.setInterpolator(ANIM_SCRIM_FADE, ALL_APPS_SCRIM_RESPONDER); - // The fact that we end on Workspace is not very ideal, but since we do, fade it in at - // the end of the transition. Don't scale/translate it. - config.setInterpolator(ANIM_WORKSPACE_FADE, clampToProgress(LINEAR, 0.8f, 1)); - config.setInterpolator(ANIM_WORKSPACE_SCALE, INSTANT); - config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, INSTANT); - } else { - // Pop the background panel, keyboard, and content in at full opacity at the threshold. - config.setInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE, - thresholdInterpolator(threshold, INSTANT)); - config.setInterpolator(ANIM_ALL_APPS_KEYBOARD_FADE, - thresholdInterpolator(threshold, INSTANT)); - config.setInterpolator(ANIM_ALL_APPS_FADE, thresholdInterpolator(threshold, INSTANT)); - - config.setInterpolator(ANIM_VERTICAL_PROGRESS, - thresholdInterpolator(threshold, mapToProgress(LINEAR, threshold, 1f))); - } - } - /** Creates an interpolator that is 0 until the threshold, then follows given interpolator. */ private static Interpolator thresholdInterpolator(float threshold, Interpolator interpolator) { return progress -> progress <= threshold ? 0 : interpolator.getInterpolation(progress); diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java index 52c358143a..faac4a3eb6 100644 --- a/src/com/android/launcher3/touch/BaseSwipeDetector.java +++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java @@ -17,6 +17,8 @@ package com.android.launcher3.touch; import static android.view.MotionEvent.INVALID_POINTER_ID; +import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent; + import android.content.Context; import android.graphics.PointF; import android.util.Log; @@ -64,6 +66,7 @@ public abstract class BaseSwipeDetector { protected PointF mSubtractDisplacement = new PointF(); @VisibleForTesting ScrollState mState = ScrollState.IDLE; private boolean mIsSettingState; + protected boolean mIsTrackpadGesture; protected boolean mIgnoreSlopWhenSettling; protected Context mContext; @@ -122,6 +125,10 @@ public abstract class BaseSwipeDetector { return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; } + public boolean isTrackpadGesture() { + return mIsTrackpadGesture; + } + public void finishedScrolling() { setState(ScrollState.IDLE); } @@ -147,7 +154,7 @@ public abstract class BaseSwipeDetector { mLastPos.set(mDownPos); mLastDisplacement.set(0, 0); mDisplacement.set(0, 0); - + mIsTrackpadGesture = isTrackpadMotionEvent(ev); if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { setState(ScrollState.DRAGGING); } diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index f46dcd328c..78709b84c8 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -46,7 +46,6 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; @@ -67,10 +66,7 @@ import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.ApiWrapper; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.views.FloatingIconView; -import com.android.launcher3.views.Snackbar; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.PendingAddShortcutInfo; -import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.PendingAppWidgetHostView; import com.android.launcher3.widget.WidgetAddFlowHandler; import com.android.launcher3.widget.WidgetManagerHelper; @@ -127,20 +123,6 @@ public class ItemClickHandler { } } else if (tag instanceof ItemClickProxy) { ((ItemClickProxy) tag).onItemClicked(v); - } else if (tag instanceof PendingAddShortcutInfo) { - CharSequence msg = Utilities.wrapForTts( - launcher.getText(R.string.long_press_shortcut_to_add), - launcher.getString(R.string.long_accessible_way_to_add_shortcut)); - Snackbar.show(launcher, msg, null); - } else if (tag instanceof PendingAddWidgetInfo) { - if (DEBUG) { - String targetPackage = ((PendingAddWidgetInfo) tag).getTargetPackage(); - Log.d(TAG, "onClick: PendingAddWidgetInfo clicked for package=" + targetPackage); - } - CharSequence msg = Utilities.wrapForTts( - launcher.getText(R.string.long_press_widget_to_add), - launcher.getString(R.string.long_accessible_way_to_add)); - Snackbar.show(launcher, msg, null); } } diff --git a/src/com/android/launcher3/util/ActivityOptionsWrapper.java b/src/com/android/launcher3/util/ActivityOptionsWrapper.java index 99cc1f7b6c..17ff2a96b7 100644 --- a/src/com/android/launcher3/util/ActivityOptionsWrapper.java +++ b/src/com/android/launcher3/util/ActivityOptionsWrapper.java @@ -25,6 +25,7 @@ import android.os.Bundle; public class ActivityOptionsWrapper { public final ActivityOptions options; + // Called when the app launch animation is complete public final RunnableList onEndCallback; public ActivityOptionsWrapper(ActivityOptions options, RunnableList onEndCallback) { diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java new file mode 100644 index 0000000000..b8cf2ae77a --- /dev/null +++ b/src/com/android/launcher3/util/DaggerSingletonObject.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.content.Context; + +import com.android.launcher3.LauncherApplication; +import com.android.launcher3.dagger.LauncherAppComponent; + +import java.util.function.Function; + +/** + * A class to provide DaggerSingleton objects in a traditional way for + * {@link MainThreadInitializedObject}. + * We should delete this class at the end and use @Inject to get dagger provided singletons. + */ + +public class DaggerSingletonObject { + private final Function mFunction; + + public DaggerSingletonObject(Function function) { + mFunction = function; + } + + public T get(Context context) { + LauncherAppComponent component = + ((LauncherApplication) context.getApplicationContext()).getAppComponent(); + return mFunction.apply(component); + } +} diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java new file mode 100644 index 0000000000..2946da1d0c --- /dev/null +++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import com.android.launcher3.dagger.LauncherAppSingleton; + +import java.util.ArrayList; + +import javax.inject.Inject; + +/** + * A tracker class for keeping track of Dagger created singletons. + * Dagger will take care of creating singletons. But we should take care of unregistering callbacks + * if at all registered during singleton construction. + * All singletons should be declared as SafeCloseable so that we can call close() method. + */ +@LauncherAppSingleton +public class DaggerSingletonTracker implements SafeCloseable { + + private final ArrayList mLauncherAppSingletons = new ArrayList<>(); + + @Inject + DaggerSingletonTracker() { + } + + /** + * Adds the SafeCloseable Singletons to the mLauncherAppSingletons list. + * This helps to track the singletons and close them appropriately. + * See {@link DaggerSingletonTracker#close()} and + * {@link MainThreadInitializedObject.SandboxContext#onDestroy()} + */ + public void addCloseable(SafeCloseable closeable) { + mLauncherAppSingletons.add(closeable); + } + + @Override + public void close() { + // Destroy in reverse order + for (int i = mLauncherAppSingletons.size() - 1; i >= 0; i--) { + mLauncherAppSingletons.get(i).close(); + } + } +} diff --git a/src/com/android/launcher3/util/DimensionUtils.kt b/src/com/android/launcher3/util/DimensionUtils.kt index 63e919ad3e..821dda789f 100644 --- a/src/com/android/launcher3/util/DimensionUtils.kt +++ b/src/com/android/launcher3/util/DimensionUtils.kt @@ -31,7 +31,8 @@ object DimensionUtils { fun getTaskbarPhoneDimensions( deviceProfile: DeviceProfile, res: Resources, - isPhoneMode: Boolean + isPhoneMode: Boolean, + isGestureNav: Boolean, ): Point { val p = Point() // Taskbar for large screen @@ -42,7 +43,7 @@ object DimensionUtils { } // Taskbar on phone using gesture nav, it will always be stashed - if (deviceProfile.isGestureMode) { + if (isGestureNav) { p.x = ViewGroup.LayoutParams.MATCH_PARENT p.y = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size) return p diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java index 16fabe26d7..c59cc81c47 100644 --- a/src/com/android/launcher3/util/DisplayController.java +++ b/src/com/android/launcher3/util/DisplayController.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.util; -import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -33,7 +32,6 @@ import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.ComponentCallbacks; import android.content.Context; import android.content.Intent; @@ -42,13 +40,13 @@ import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; -import android.os.Build; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.view.Display; import androidx.annotation.AnyThread; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; @@ -76,6 +74,7 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { private static final String TAG = "DisplayController"; private static final boolean DEBUG = false; + private static boolean sTaskbarModePreferenceStatusForTests = false; private static boolean sTransientTaskbarStatusForTests = true; // TODO(b/254119092) remove all logs with this tag @@ -109,7 +108,10 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { private DisplayInfoChangeListener mPriorityListener; private final ArrayList mListeners = new ArrayList<>(); - private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent); + // We will register broadcast receiver on main thread to ensure not missing changes on + // TARGET_OVERLAY_PACKAGE and ACTION_OVERLAY_CHANGED. + private final SimpleBroadcastReceiver mReceiver = + new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::onIntent); private Info mInfo; private boolean mDestroyed = false; @@ -127,21 +129,15 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { } Display display = mDM.getDisplay(DEFAULT_DISPLAY); - if (Utilities.ATLEAST_S) { - mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null); - mWindowContext.registerComponentCallbacks(this); - } else { - mWindowContext = null; - mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED); - } + mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null); + mWindowContext.registerComponentCallbacks(this); // Initialize navigation mode change listener mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED); WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context); - Context displayInfoContext = getDisplayInfoContext(display); - mInfo = new Info(displayInfoContext, wmProxy, - wmProxy.estimateInternalDisplayBounds(displayInfoContext)); + mInfo = new Info(mWindowContext, wmProxy, + wmProxy.estimateInternalDisplayBounds(mWindowContext)); FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds); } @@ -156,7 +152,7 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get( TASKBAR_PINNING_IN_DESKTOP_MODE); if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) { - handleInfoChange(mWindowContext.getDisplay()); + notifyConfigChange(); } }; @@ -182,13 +178,6 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return INSTANCE.get(context).getInfo().isTransientTaskbar(); } - /** - * Handles info change for desktop mode. - */ - public static void handleInfoChangeForDesktopMode(Context context) { - INSTANCE.get(context).handleInfoChange(context.getDisplay()); - } - /** * Enables transient taskbar status for tests. */ @@ -197,6 +186,14 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { sTransientTaskbarStatusForTests = enable; } + /** + * Enables respecting taskbar mode preference during test. + */ + @VisibleForTesting + public static void enableTaskbarModePreferenceForTests(boolean enable) { + sTaskbarModePreferenceStatusForTests = enable; + } + /** * Returns whether the taskbar is pinned in gesture navigation mode. */ @@ -204,6 +201,13 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return INSTANCE.get(context).getInfo().isPinnedTaskbar(); } + /** + * Returns whether the taskbar is forced to be pinned when home is visible. + */ + public static boolean showLockedTaskbarOnHome(Context context) { + return INSTANCE.get(context).getInfo().showLockedTaskbarOnHome(); + } + @Override public void close() { mDestroyed = true; @@ -218,6 +222,7 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { } else { // TODO: unregister broadcast receiver } + mReceiver.unregisterReceiverSafely(mContext); } /** @@ -238,36 +243,22 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { if (mDestroyed) { return; } - boolean reconfigure = false; if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) { - reconfigure = true; - } else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { - Configuration config = mContext.getResources().getConfiguration(); - reconfigure = mInfo.fontScale != config.fontScale - || mInfo.densityDpi != config.densityDpi; - } - - if (reconfigure) { - Log.d(TAG, "Configuration changed, notifying listeners"); - Display display = mDM.getDisplay(DEFAULT_DISPLAY); - if (display != null) { - handleInfoChange(display); - } + Log.d(TAG, "Overlay changed, notifying listeners"); + notifyConfigChange(); } } @UiThread @Override - @TargetApi(Build.VERSION_CODES.S) public final void onConfigurationChanged(Configuration config) { Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config); - Display display = mWindowContext.getDisplay(); if (config.densityDpi != mInfo.densityDpi || config.fontScale != mInfo.fontScale - || display.getRotation() != mInfo.rotation || !mInfo.mScreenSizeDp.equals( - new PortraitSize(config.screenHeightDp, config.screenWidthDp))) { - handleInfoChange(display); + new PortraitSize(config.screenHeightDp, config.screenWidthDp)) + || mWindowContext.getDisplay().getRotation() != mInfo.rotation) { + notifyConfigChange(); } } @@ -290,17 +281,12 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return mInfo; } - private Context getDisplayInfoContext(Display display) { - return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display); - } - @AnyThread - @VisibleForTesting - public void handleInfoChange(Display display) { + public void notifyConfigChange() { WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext); Info oldInfo = mInfo; - Context displayInfoContext = getDisplayInfoContext(display); + Context displayInfoContext = mWindowContext; Info newInfo = new Info(displayInfoContext, wmProxy, oldInfo.mPerDisplayBounds); if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale @@ -331,7 +317,8 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { } if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) || (newInfo.mIsTaskbarPinnedInDesktopMode - != oldInfo.mIsTaskbarPinnedInDesktopMode)) { + != oldInfo.mIsTaskbarPinnedInDesktopMode) + || newInfo.isPinnedTaskbar() != oldInfo.isPinnedTaskbar()) { change |= CHANGE_TASKBAR_PINNING; } if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) { @@ -385,6 +372,9 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { private final boolean mIsInDesktopMode; + private final boolean mShowLockedTaskbarOnHome; + private final boolean mIsHomeVisible; + public Info(Context displayInfoContext) { /* don't need system overrides for external displays */ this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>()); @@ -446,6 +436,8 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get( TASKBAR_PINNING_IN_DESKTOP_MODE); mIsInDesktopMode = wmProxy.isInDesktopMode(); + mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext); + mIsHomeVisible = wmProxy.isHomeVisible(displayInfoContext); } /** @@ -455,13 +447,17 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { if (navigationMode != NavigationMode.NO_BUTTON) { return false; } - if (Utilities.isRunningInTestHarness()) { + if (Utilities.isRunningInTestHarness() && !sTaskbarModePreferenceStatusForTests) { // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of // sTransientTaskbarStatusForTests and update test to directly // toggle shared preference to switch transient taskbar on/off. return sTransientTaskbarStatusForTests; } if (enableTaskbarPinning()) { + // If Launcher is visible on the freeform display, ensure the taskbar is pinned. + if (mShowLockedTaskbarOnHome && mIsHomeVisible) { + return false; + } if (mIsInDesktopMode) { return !mIsTaskbarPinnedInDesktopMode; } @@ -477,10 +473,6 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar(); } - public boolean isInDesktopMode() { - return mIsInDesktopMode; - } - /** * Returns {@code true} if the bounds represent a tablet. */ @@ -507,9 +499,8 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return Collections.unmodifiableSet(mPerDisplayBounds.keySet()); } - /** - * Returns all {@link WindowBounds}s for the current display. - */ + /** Returns all {@link WindowBounds}s for the current display. */ + @Nullable public List getCurrentBounds() { return mPerDisplayBounds.get(normalizedDisplayInfo); } @@ -534,6 +525,13 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { return TYPE_PHONE; } } + + /** + * Returns whether the taskbar is forced to be pinned when home is visible. + */ + public boolean showLockedTaskbarOnHome() { + return mShowLockedTaskbarOnHome; + } } /** diff --git a/src/com/android/launcher3/util/EdgeEffectCompat.java b/src/com/android/launcher3/util/EdgeEffectCompat.java index ca3725923e..a949f50ca1 100644 --- a/src/com/android/launcher3/util/EdgeEffectCompat.java +++ b/src/com/android/launcher3/util/EdgeEffectCompat.java @@ -19,8 +19,6 @@ import android.content.Context; import android.view.MotionEvent; import android.widget.EdgeEffect; -import com.android.launcher3.Utilities; - /** * Extension of {@link EdgeEffect} to allow backwards compatibility */ @@ -30,21 +28,6 @@ public class EdgeEffectCompat extends EdgeEffect { super(context); } - @Override - public float getDistance() { - return Utilities.ATLEAST_S ? super.getDistance() : 0; - } - - @Override - public float onPullDistance(float deltaDistance, float displacement) { - if (Utilities.ATLEAST_S) { - return super.onPullDistance(deltaDistance, displacement); - } else { - onPull(deltaDistance, displacement); - return deltaDistance; - } - } - public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) { return onPullDistance(deltaDistance, displacement); } diff --git a/src/com/android/launcher3/util/ExecutorUtil.java b/src/com/android/launcher3/util/ExecutorUtil.java new file mode 100644 index 0000000000..efc0eec194 --- /dev/null +++ b/src/com/android/launcher3/util/ExecutorUtil.java @@ -0,0 +1,37 @@ +/* + * 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.launcher3.util.Executors.MAIN_EXECUTOR; + +import android.os.Looper; + +import java.util.concurrent.ExecutionException; + +public final class ExecutorUtil { + + /** + * Executes runnable on {@link Looper#getMainLooper()}, otherwise fails with an exception. + */ + public static void executeSyncOnMainOrFail(Runnable runnable) { + try { + MAIN_EXECUTOR.submit(runnable).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt index 94f9e4fb69..10559f3489 100644 --- a/src/com/android/launcher3/util/LockedUserState.kt +++ b/src/com/android/launcher3/util/LockedUserState.kt @@ -20,20 +20,28 @@ import android.content.Intent import android.os.Process import android.os.UserManager import androidx.annotation.VisibleForTesting +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR class LockedUserState(private val mContext: Context) : SafeCloseable { val isUserUnlockedAtLauncherStartup: Boolean - var isUserUnlocked: Boolean - private set + var isUserUnlocked = false + private set(value) { + field = value + if (value) { + notifyUserUnlocked() + } + } + private val mUserUnlockedActions: RunnableList = RunnableList() @VisibleForTesting - val mUserUnlockedReceiver = SimpleBroadcastReceiver { - if (Intent.ACTION_USER_UNLOCKED == it.action) { - isUserUnlocked = true - notifyUserUnlocked() + val mUserUnlockedReceiver = + SimpleBroadcastReceiver(UI_HELPER_EXECUTOR) { + if (Intent.ACTION_USER_UNLOCKED == it.action) { + isUserUnlocked = true + } } - } init { // 1) when user reboots devices, launcher process starts at lock screen and both @@ -42,30 +50,34 @@ class LockedUserState(private val mContext: Context) : SafeCloseable { // yet isUserUnlockedAtLauncherStartup will remains as false. // 2) when launcher process restarts after user has unlocked screen, both variable are // init as true and will not change. - isUserUnlocked = - mContext - .getSystemService(UserManager::class.java)!! - .isUserUnlocked(Process.myUserHandle()) + isUserUnlocked = checkIsUserUnlocked() isUserUnlockedAtLauncherStartup = isUserUnlocked - if (isUserUnlocked) { - notifyUserUnlocked() - } else { - mUserUnlockedReceiver.register(mContext, Intent.ACTION_USER_UNLOCKED) + if (!isUserUnlocked) { + mUserUnlockedReceiver.register( + mContext, + { + // If user is unlocked while registering broadcast receiver, we should update + // [isUserUnlocked], which will call [notifyUserUnlocked] in setter + if (checkIsUserUnlocked()) { + MAIN_EXECUTOR.execute { isUserUnlocked = true } + } + }, + Intent.ACTION_USER_UNLOCKED + ) } } + private fun checkIsUserUnlocked() = + mContext.getSystemService(UserManager::class.java)!!.isUserUnlocked(Process.myUserHandle()) + private fun notifyUserUnlocked() { mUserUnlockedActions.executeAllAndDestroy() - Executors.THREAD_POOL_EXECUTOR.execute { - mUserUnlockedReceiver.unregisterReceiverSafely(mContext) - } + mUserUnlockedReceiver.unregisterReceiverSafely(mContext) } /** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */ override fun close() { - Executors.THREAD_POOL_EXECUTOR.execute { - mUserUnlockedReceiver.unregisterReceiverSafely(mContext) - } + mUserUnlockedReceiver.unregisterReceiverSafely(mContext) } /** diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java index 3d4b409cf8..f183f18050 100644 --- a/src/com/android/launcher3/util/LogConfig.java +++ b/src/com/android/launcher3/util/LogConfig.java @@ -76,4 +76,5 @@ public class LogConfig { * When turned on, we enable zero state web data loader related logging. */ public static final String ZERO_WEB_DATA_LOADER = "ZeroStateWebDataLoaderLog"; + public static final String SEARCH_TARGET_UTIL_LOG = "SearchTargetUtilLog"; } diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java index 1a0f9a0a8b..e12ccbcf70 100644 --- a/src/com/android/launcher3/util/MainThreadInitializedObject.java +++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java @@ -18,13 +18,13 @@ package com.android.launcher3.util; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; -import android.content.ContextWrapper; import android.os.Looper; import android.util.Log; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import com.android.launcher3.LauncherApplication; import com.android.launcher3.util.ResourceBasedOverride.Overrides; import java.util.ArrayList; @@ -35,6 +35,9 @@ import java.util.function.Consumer; /** * Utility class for defining singletons which are initiated on main thread. + * + * TODO(b/361850561): Do not delete MainThreadInitializedObject until we find a way to + * unregister and understand how singleton objects are destroyed in dagger graph. */ public class MainThreadInitializedObject { @@ -115,7 +118,7 @@ public class MainThreadInitializedObject { * Abstract Context which allows custom implementations for * {@link MainThreadInitializedObject} providers */ - public static class SandboxContext extends ContextWrapper implements SandboxApplication { + public static class SandboxContext extends LauncherApplication implements SandboxApplication { private static final String TAG = "SandboxContext"; @@ -126,7 +129,8 @@ public class MainThreadInitializedObject { private boolean mDestroyed = false; public SandboxContext(Context base) { - super(base); + attachBaseContext(base); + initDagger(); } @Override @@ -135,6 +139,7 @@ public class MainThreadInitializedObject { } public void onDestroy() { + getAppComponent().getDaggerSingletonTracker().close(); synchronized (mDestroyLock) { // Destroy in reverse order for (int i = mOrderedObjects.size() - 1; i >= 0; i--) { diff --git a/src/com/android/launcher3/util/MultiTranslateDelegate.java b/src/com/android/launcher3/util/MultiTranslateDelegate.java index 84ef445dcf..38c87c8d5a 100644 --- a/src/com/android/launcher3/util/MultiTranslateDelegate.java +++ b/src/com/android/launcher3/util/MultiTranslateDelegate.java @@ -37,6 +37,7 @@ public class MultiTranslateDelegate { public static final int INDEX_TASKBAR_ALIGNMENT_ANIM = 3; public static final int INDEX_TASKBAR_REVEAL_ANIM = 4; public static final int INDEX_TASKBAR_PINNING_ANIM = 5; + public static final int INDEX_NAV_BAR_ANIM = 6; // Affect all items inside of a MultipageCellLayout public static final int INDEX_CELLAYOUT_MULTIPAGE_SPACING = 3; @@ -47,7 +48,7 @@ public class MultiTranslateDelegate { // Specific for hotseat items when adjusting for bubbles public static final int INDEX_BUBBLE_ADJUSTMENT_ANIM = 3; - public static final int COUNT = 6; + public static final int COUNT = 7; private final MultiPropertyFactory mTranslationX; private final MultiPropertyFactory mTranslationY; diff --git a/src/com/android/launcher3/util/OnboardingPrefs.kt b/src/com/android/launcher3/util/OnboardingPrefs.kt index ac6e97c5b2..771594ea9d 100644 --- a/src/com/android/launcher3/util/OnboardingPrefs.kt +++ b/src/com/android/launcher3/util/OnboardingPrefs.kt @@ -16,6 +16,7 @@ package com.android.launcher3.util import android.content.Context +import androidx.annotation.VisibleForTesting import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherPrefs.Companion.backedUpItem @@ -26,7 +27,7 @@ object OnboardingPrefs { val sharedPrefKey: String, val maxCount: Int, ) { - private val prefItem = backedUpItem(sharedPrefKey, 0) + @VisibleForTesting val prefItem = backedUpItem(sharedPrefKey, 0) /** @return The number of times we have seen the given event. */ fun get(c: Context): Int { diff --git a/src/com/android/launcher3/util/OverlayEdgeEffect.java b/src/com/android/launcher3/util/OverlayEdgeEffect.java index d09d801d66..0623af7e43 100644 --- a/src/com/android/launcher3/util/OverlayEdgeEffect.java +++ b/src/com/android/launcher3/util/OverlayEdgeEffect.java @@ -46,6 +46,7 @@ public class OverlayEdgeEffect extends EdgeEffectCompat { return mDistance; } + @Override public float onPullDistance(float deltaDistance, float displacement) { // Fallback implementation, will never actually get called if (BuildConfig.IS_DEBUG_DEVICE) { diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index 8c5a76e665..b1913c0164 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -212,7 +212,7 @@ public class PackageManagerHelper implements SafeCloseable{ if (info instanceof ItemInfoWithIcon appInfo && (appInfo.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) { context.startActivity(ApiWrapper.INSTANCE.get(context).getAppMarketActivityIntent( - appInfo.getTargetComponent().getPackageName(), Process.myUserHandle())); + appInfo.getTargetComponent().getPackageName(), Process.myUserHandle()), opts); return; } ComponentName componentName = null; @@ -303,10 +303,7 @@ public class PackageManagerHelper implements SafeCloseable{ /** Returns the incremental download progress for the given shortcut's app. */ public static int getLoadingProgress(LauncherActivityInfo info) { - if (Utilities.ATLEAST_S) { - return (int) (100 * info.getLoadingProgress()); - } - return 100; + return (int) (100 * info.getLoadingProgress()); } /** Returns true in case app is installed on the device or in archived state. */ diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java index e16e4778e9..8ee799a2dd 100644 --- a/src/com/android/launcher3/util/ScreenOnTracker.java +++ b/src/com/android/launcher3/util/ScreenOnTracker.java @@ -19,10 +19,15 @@ import static android.content.Intent.ACTION_SCREEN_OFF; import static android.content.Intent.ACTION_SCREEN_ON; import static android.content.Intent.ACTION_USER_PRESENT; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + import android.content.Context; import android.content.Intent; +import androidx.annotation.VisibleForTesting; + import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; /** * Utility class for tracking if the screen is currently on or off @@ -32,7 +37,7 @@ public class ScreenOnTracker implements SafeCloseable { public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>(ScreenOnTracker::new); - private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onReceive); + private final SimpleBroadcastReceiver mReceiver; private final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); private final Context mContext; @@ -41,8 +46,20 @@ public class ScreenOnTracker implements SafeCloseable { private ScreenOnTracker(Context context) { // Assume that the screen is on to begin with mContext = context; + mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive); + init(); + } + + @VisibleForTesting + ScreenOnTracker(Context context, SimpleBroadcastReceiver receiver) { + mContext = context; + mReceiver = receiver; + init(); + } + + private void init() { mIsScreenOn = true; - mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT); + mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT); } @Override @@ -50,7 +67,8 @@ public class ScreenOnTracker implements SafeCloseable { mReceiver.unregisterReceiverSafely(mContext); } - private void onReceive(Intent intent) { + @VisibleForTesting + void onReceive(Intent intent) { String action = intent.getAction(); if (ACTION_SCREEN_ON.equals(action)) { mIsScreenOn = true; diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java index ccd154a570..cd6701df93 100644 --- a/src/com/android/launcher3/util/SettingsCache.java +++ b/src/com/android/launcher3/util/SettingsCache.java @@ -18,6 +18,8 @@ package com.android.launcher3.util; import static android.provider.Settings.System.ACCELEROMETER_ROTATION; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -87,7 +89,7 @@ public class SettingsCache extends ContentObserver implements SafeCloseable { @Override public void close() { - mResolver.unregisterContentObserver(this); + UI_HELPER_EXECUTOR.execute(() -> mResolver.unregisterContentObserver(this)); } @Override @@ -135,7 +137,8 @@ public class SettingsCache extends ContentObserver implements SafeCloseable { CopyOnWriteArrayList l = new CopyOnWriteArrayList<>(); l.add(changeListener); mListenerMap.put(uri, l); - mResolver.registerContentObserver(uri, false, this); + UI_HELPER_EXECUTOR.execute( + () -> mResolver.registerContentObserver(uri, false, this)); } } diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java index 07b7941036..aa4f8af7ba 100644 --- a/src/com/android/launcher3/util/ShortcutUtil.java +++ b/src/com/android/launcher3/util/ShortcutUtil.java @@ -54,14 +54,6 @@ public class ShortcutUtil { ? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY; } - /** - * Returns true if the item is a deep shortcut. - */ - public static boolean isDeepShortcut(ItemInfo info) { - return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT - && info instanceof WorkspaceItemInfo; - } - private static boolean isActive(ItemInfo info) { boolean isLoading = info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi(); diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java index 064bcd072f..539a7cb8ec 100644 --- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java +++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java @@ -19,9 +19,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; import android.os.PatternMatcher; import android.text.TextUtils; +import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import java.util.function.Consumer; @@ -30,8 +33,16 @@ public class SimpleBroadcastReceiver extends BroadcastReceiver { private final Consumer mIntentConsumer; - public SimpleBroadcastReceiver(Consumer intentConsumer) { + // Handler to register/unregister broadcast receiver + private final Handler mHandler; + + public SimpleBroadcastReceiver(LooperExecutor looperExecutor, Consumer intentConsumer) { + this(looperExecutor.getHandler(), intentConsumer); + } + + public SimpleBroadcastReceiver(Handler handler, Consumer intentConsumer) { mIntentConsumer = intentConsumer; + mHandler = handler; } @Override @@ -39,18 +50,109 @@ public class SimpleBroadcastReceiver extends BroadcastReceiver { mIntentConsumer.accept(intent); } - /** - * Helper method to register multiple actions - */ + /** Calls {@link #register(Context, Runnable, String...)} with null completionCallback. */ + @AnyThread public void register(Context context, String... actions) { - context.registerReceiver(this, getFilter(actions)); + register(context, null, actions); } /** - * Helper method to register multiple actions associated with a paction + * Calls {@link #register(Context, Runnable, int, String...)} with null completionCallback. */ + @AnyThread + public void register(Context context, int flags, String... actions) { + register(context, null, flags, actions); + } + + /** + * Register broadcast receiver. If this method is called on the same looper with mHandler's + * looper, then register will be called synchronously. Otherwise asynchronously. This ensures + * register happens on {@link #mHandler}'s looper. + * + * @param completionCallback callback that will be triggered after registration is completed, + * caller usually pass this callback to check if states has changed + * while registerReceiver() is executed on a binder call. + */ + @AnyThread + public void register( + Context context, @Nullable Runnable completionCallback, String... actions) { + if (Looper.myLooper() == mHandler.getLooper()) { + registerInternal(context, completionCallback, actions); + } else { + mHandler.post(() -> registerInternal(context, completionCallback, actions)); + } + } + + /** Register broadcast receiver and run completion callback if passed. */ + @AnyThread + private void registerInternal( + Context context, @Nullable Runnable completionCallback, String... actions) { + context.registerReceiver(this, getFilter(actions)); + if (completionCallback != null) { + completionCallback.run(); + } + } + + /** + * Same as {@link #register(Context, Runnable, String...)} above but with additional flags + * params. + */ + @AnyThread + public void register( + Context context, @Nullable Runnable completionCallback, int flags, String... actions) { + if (Looper.myLooper() == mHandler.getLooper()) { + registerInternal(context, completionCallback, flags, actions); + } else { + mHandler.post(() -> registerInternal(context, completionCallback, flags, actions)); + } + } + + /** Register broadcast receiver and run completion callback if passed. */ + @AnyThread + private void registerInternal( + Context context, @Nullable Runnable completionCallback, int flags, String... actions) { + context.registerReceiver(this, getFilter(actions), flags); + if (completionCallback != null) { + completionCallback.run(); + } + } + + /** Same as {@link #register(Context, Runnable, String...)} above but with pkg name. */ + @AnyThread public void registerPkgActions(Context context, @Nullable String pkg, String... actions) { - context.registerReceiver(this, getPackageFilter(pkg, actions)); + if (Looper.myLooper() == mHandler.getLooper()) { + context.registerReceiver(this, getPackageFilter(pkg, actions)); + } else { + mHandler.post(() -> { + context.registerReceiver(this, getPackageFilter(pkg, actions)); + }); + } + } + + /** + * Unregister broadcast receiver. If this method is called on the same looper with mHandler's + * looper, then unregister will be called synchronously. Otherwise asynchronously. This ensures + * unregister happens on {@link #mHandler}'s looper. + */ + @AnyThread + public void unregisterReceiverSafely(Context context) { + if (Looper.myLooper() == mHandler.getLooper()) { + unregisterReceiverSafelyInternal(context); + } else { + mHandler.post(() -> { + unregisterReceiverSafelyInternal(context); + }); + } + } + + /** Unregister broadcast receiver ignoring any errors. */ + @AnyThread + private void unregisterReceiverSafelyInternal(Context context) { + try { + context.unregisterReceiver(this); + } catch (IllegalArgumentException e) { + // It was probably never registered or already unregistered. Ignore. + } } /** @@ -72,15 +174,4 @@ public class SimpleBroadcastReceiver extends BroadcastReceiver { } return filter; } - - /** - * Unregisters the receiver ignoring any errors - */ - public void unregisterReceiverSafely(Context context) { - try { - context.unregisterReceiver(this); - } catch (IllegalArgumentException e) { - // It was probably never registered or already unregistered. Ignore. - } - } } diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java index 95624b1cb3..f457e4e369 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -186,12 +186,6 @@ public final class SplitConfigurationOptions { public int stagePosition = STAGE_POSITION_UNDEFINED; @StageType public int stageType = STAGE_TYPE_UNDEFINED; - - @Override - public String toString() { - return "SplitStageInfo { taskId=" + taskId - + ", stagePosition=" + stagePosition + ", stageType=" + stageType + " }"; - } } public static StatsLogManager.EventEnum getLogEventForPosition(@StagePosition int position) { @@ -217,7 +211,7 @@ public final class SplitConfigurationOptions { private Drawable drawable; public final Intent intent; public final SplitPositionOption position; - public final ItemInfo itemInfo; + private ItemInfo itemInfo; public final StatsLogManager.EventEnum splitEvent; /** Represents the taskId of the first app to start in split screen */ public int alreadyRunningTaskId = INVALID_TASK_ID; @@ -245,5 +239,9 @@ public final class SplitConfigurationOptions { public View getView() { return view; } + + public ItemInfo getItemInfo() { + return itemInfo; + } } } diff --git a/src/com/android/launcher3/util/StableViewInfo.kt b/src/com/android/launcher3/util/StableViewInfo.kt new file mode 100644 index 0000000000..29dcd59711 --- /dev/null +++ b/src/com/android/launcher3/util/StableViewInfo.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.launcher3.util + +import android.os.IBinder +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.ItemInfo.NO_ID + +/** Info parameters that can be used to identify a Launcher object */ +data class StableViewInfo(val itemId: Int, val containerId: Int, val stableId: Any) { + + fun matches(info: ItemInfo?) = + info != null && + itemId == info.id && + containerId == info.container && + stableId == info.stableId + + companion object { + + private fun ItemInfo.toStableViewInfo() = + stableId?.let { sId -> + if (id != NO_ID || container != NO_ID) StableViewInfo(id, container, sId) else null + } + + /** + * Return a new launch cookie for the activity launch if supported. + * + * @param info the item info for the launch + */ + @JvmStatic + fun toLaunchCookie(info: ItemInfo?) = + info?.toStableViewInfo()?.let { ObjectWrapper.wrap(it) } + + /** + * Unwraps the binder and returns the first non-null StableViewInfo in the list or null if + * none can be found + */ + @JvmStatic + fun fromLaunchCookies(launchCookies: List) = + launchCookies.firstNotNullOfOrNull { ObjectWrapper.unwrap(it) } + } +} diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java index 60951ba05a..104040afff 100644 --- a/src/com/android/launcher3/util/Themes.java +++ b/src/com/android/launcher3/util/Themes.java @@ -52,10 +52,8 @@ public class Themes { } public static int getActivityThemeRes(Context context, int wallpaperColorHints) { - boolean supportsDarkText = Utilities.ATLEAST_S - && (wallpaperColorHints & HINT_SUPPORTS_DARK_TEXT) != 0; - boolean isMainColorDark = Utilities.ATLEAST_S - && (wallpaperColorHints & HINT_SUPPORTS_DARK_THEME) != 0; + boolean supportsDarkText = (wallpaperColorHints & HINT_SUPPORTS_DARK_TEXT) != 0; + boolean isMainColorDark = (wallpaperColorHints & HINT_SUPPORTS_DARK_THEME) != 0; if (Utilities.isDarkTheme(context)) { return supportsDarkText ? R.style.AppTheme_Dark_DarkText diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java index 51749a7f85..adb8f9d053 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/src/com/android/launcher3/util/VibratorWrapper.java @@ -25,14 +25,11 @@ import android.annotation.SuppressLint; import android.content.Context; import android.media.AudioAttributes; import android.net.Uri; -import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; -import androidx.annotation.Nullable; - -import com.android.launcher3.Utilities; +import androidx.annotation.VisibleForTesting; /** * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. @@ -49,122 +46,48 @@ public class VibratorWrapper implements SafeCloseable { public static final VibrationEffect EFFECT_CLICK = createPredefined(VibrationEffect.EFFECT_CLICK); - private static final Uri HAPTIC_FEEDBACK_URI = - Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); + @VisibleForTesting + static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); - private static final float LOW_TICK_SCALE = 0.9f; - private static final float DRAG_TEXTURE_SCALE = 0.03f; - private static final float DRAG_COMMIT_SCALE = 0.5f; - private static final float DRAG_BUMP_SCALE = 0.4f; - private static final int DRAG_TEXTURE_EFFECT_SIZE = 200; - - @Nullable - private final VibrationEffect mDragEffect; - @Nullable - private final VibrationEffect mCommitEffect; - @Nullable - private final VibrationEffect mBumpEffect; - - private long mLastDragTime; - private final int mThresholdUntilNextDragCallMillis; + @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f; /** * Haptic when entering overview. */ public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; - private final Context mContext; private final Vibrator mVibrator; private final boolean mHasVibrator; - private final SettingsCache.OnChangeListener mHapticChangeListener = + + private final SettingsCache mSettingsCache; + + @VisibleForTesting + final SettingsCache.OnChangeListener mHapticChangeListener = isEnabled -> mIsHapticFeedbackEnabled = isEnabled; private boolean mIsHapticFeedbackEnabled; private VibratorWrapper(Context context) { - mContext = context; - mVibrator = context.getSystemService(Vibrator.class); + this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context)); + } + + @VisibleForTesting + VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) { + mVibrator = vibrator; mHasVibrator = mVibrator.hasVibrator(); + mSettingsCache = settingsCache; if (mHasVibrator) { - SettingsCache cache = SettingsCache.INSTANCE.get(mContext); - cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener); - mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0); + mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener); + mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0); } else { mIsHapticFeedbackEnabled = false; } - - if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported( - PRIMITIVE_LOW_TICK)) { - - // Drag texture, Commit, and Bump should only be used for premium phones. - // Before using these haptics make sure check if the device can use it - VibrationEffect.Composition dragEffect = VibrationEffect.startComposition(); - for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) { - dragEffect.addPrimitive( - PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE); - } - mDragEffect = dragEffect.compose(); - mCommitEffect = VibrationEffect.startComposition().addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose(); - mBumpEffect = VibrationEffect.startComposition().addPrimitive( - PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose(); - int primitiveDuration = mVibrator.getPrimitiveDurations( - PRIMITIVE_LOW_TICK)[0]; - - mThresholdUntilNextDragCallMillis = - DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100; - } else { - mDragEffect = null; - mCommitEffect = null; - mBumpEffect = null; - mThresholdUntilNextDragCallMillis = 0; - } } @Override public void close() { if (mHasVibrator) { - SettingsCache.INSTANCE.get(mContext) - .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener); - } - } - - /** - * This is called when the user swipes to/from all apps. This is meant to be used in between - * long animation progresses so that it gives a dragging texture effect. For a better - * experience, this should be used in combination with vibrateForDragCommit(). - */ - public void vibrateForDragTexture() { - if (mDragEffect == null) { - return; - } - long currentTime = SystemClock.elapsedRealtime(); - long elapsedTimeSinceDrag = currentTime - mLastDragTime; - if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) { - vibrate(mDragEffect); - mLastDragTime = currentTime; - } - } - - /** - * This is used when user reaches the commit threshold when swiping to/from from all apps. - */ - public void vibrateForDragCommit() { - if (mCommitEffect != null) { - vibrate(mCommitEffect); - } - // resetting dragTexture timestamp to be able to play dragTexture again - mLastDragTime = 0; - } - - /** - * The bump haptic is used to be called at the end of a swipe and only if it the gesture is a - * FLING going to/from all apps. Client can just call this method elsewhere just for the - * effect. - */ - public void vibrateForDragBump() { - if (mBumpEffect != null) { - vibrate(mBumpEffect); + mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener); } } @@ -174,8 +97,6 @@ public class VibratorWrapper implements SafeCloseable { */ public void cancelVibrate() { UI_HELPER_EXECUTOR.execute(mVibrator::cancel); - // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled - mLastDragTime = 0; } /** Vibrates with the given effect if haptic feedback is available and enabled. */ @@ -206,7 +127,7 @@ public class VibratorWrapper implements SafeCloseable { /** Indicates that Taskbar has been invoked. */ public void vibrateForTaskbarUnstash() { - if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { + if (mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { VibrationEffect primitiveLowTickEffect = VibrationEffect .startComposition() .addPrimitive(PRIMITIVE_LOW_TICK, LOW_TICK_SCALE) diff --git a/src/com/android/launcher3/util/ViewCache.java b/src/com/android/launcher3/util/ViewCache.java index 98e6822542..b98e977dd4 100644 --- a/src/com/android/launcher3/util/ViewCache.java +++ b/src/com/android/launcher3/util/ViewCache.java @@ -21,6 +21,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.R; /** @@ -67,7 +69,8 @@ public class ViewCache { } } - private static class CacheEntry { + @VisibleForTesting + static class CacheEntry { final int mMaxSize; final View[] mViews; diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java index e413d7ff9c..2fa8bf4681 100644 --- a/src/com/android/launcher3/util/ViewPool.java +++ b/src/com/android/launcher3/util/ViewPool.java @@ -24,6 +24,7 @@ import android.view.ViewGroup; import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.util.ViewPool.Reusable; @@ -43,9 +44,16 @@ public class ViewPool { public ViewPool(Context context, @Nullable ViewGroup parent, int layoutId, int maxSize, int initialSize) { + this(LayoutInflater.from(context).cloneInContext(context), + parent, layoutId, maxSize, initialSize); + } + + @VisibleForTesting + ViewPool(LayoutInflater inflater, @Nullable ViewGroup parent, + int layoutId, int maxSize, int initialSize) { mLayoutId = layoutId; mParent = parent; - mInflater = LayoutInflater.from(context); + mInflater = inflater; mPool = new Object[maxSize]; if (initialSize > 0) { diff --git a/src/com/android/launcher3/util/WallpaperColorHints.kt b/src/com/android/launcher3/util/WallpaperColorHints.kt index 1361c1ed21..11d4c25689 100644 --- a/src/com/android/launcher3/util/WallpaperColorHints.kt +++ b/src/com/android/launcher3/util/WallpaperColorHints.kt @@ -23,7 +23,6 @@ import android.app.WallpaperManager.OnColorsChangedListener import android.content.Context import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting -import com.android.launcher3.Utilities import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR @@ -34,36 +33,34 @@ import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR class WallpaperColorHints(private val context: Context) : SafeCloseable { var hints: Int = 0 private set + private val wallpaperManager get() = context.getSystemService(WallpaperManager::class.java)!! + private val onColorHintsChangedListeners = mutableListOf() private val onClose: SafeCloseable init { - if (Utilities.ATLEAST_S) { - hints = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)?.colorHints ?: 0 - val onColorsChangedListener = OnColorsChangedListener { colors, which -> - onColorsChanged(colors, which) - } + hints = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)?.colorHints ?: 0 + val onColorsChangedListener = OnColorsChangedListener { colors, which -> + onColorsChanged(colors, which) + } + UI_HELPER_EXECUTOR.execute { + wallpaperManager.addOnColorsChangedListener( + onColorsChangedListener, + MAIN_EXECUTOR.handler, + ) + } + onClose = SafeCloseable { UI_HELPER_EXECUTOR.execute { - wallpaperManager.addOnColorsChangedListener( - onColorsChangedListener, - MAIN_EXECUTOR.handler - ) + wallpaperManager.removeOnColorsChangedListener(onColorsChangedListener) } - onClose = SafeCloseable { - UI_HELPER_EXECUTOR.execute { - wallpaperManager.removeOnColorsChangedListener(onColorsChangedListener) - } - } - } else { - onClose = SafeCloseable {} } } @MainThread private fun onColorsChanged(colors: WallpaperColors?, which: Int) { - if ((which and FLAG_SYSTEM) != 0 && Utilities.ATLEAST_S) { + if ((which and FLAG_SYSTEM) != 0) { val newHints = colors?.colorHints ?: 0 if (newHints != hints) { hints = newHints @@ -86,6 +83,7 @@ class WallpaperColorHints(private val context: Context) : SafeCloseable { @VisibleForTesting @JvmField val INSTANCE = MainThreadInitializedObject { WallpaperColorHints(it) } + @JvmStatic fun get(context: Context): WallpaperColorHints = INSTANCE.get(context) } } diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java index b97b8894c7..f8cbe0d833 100644 --- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java +++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java @@ -32,7 +32,7 @@ public class WallpaperOffsetInterpolator { private static final int MIN_PARALLAX_PAGE_SPAN = 4; private final SimpleBroadcastReceiver mWallpaperChangeReceiver = - new SimpleBroadcastReceiver(i -> onWallpaperChanged()); + new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> onWallpaperChanged()); private final Workspace mWorkspace; private final boolean mIsRtl; private final Handler mHandler; @@ -201,7 +201,8 @@ public class WallpaperOffsetInterpolator { mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext()); mRegistered = false; } else if (mWindowToken != null && !mRegistered) { - mWallpaperChangeReceiver.register(mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED); + mWallpaperChangeReceiver.register( + mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED); onWallpaperChanged(); mRegistered = true; } diff --git a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt new file mode 100644 index 0000000000..e9691a85c0 --- /dev/null +++ b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt @@ -0,0 +1,34 @@ +/* + * 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.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +interface DispatcherProvider { + val default: CoroutineDispatcher + val io: CoroutineDispatcher + val main: CoroutineDispatcher + val unconfined: CoroutineDispatcher +} + +object ProductionDispatchers : DispatcherProvider { + override val default: CoroutineDispatcher = Dispatchers.Default + override val io: CoroutineDispatcher = Dispatchers.IO + override val main: CoroutineDispatcher = Dispatchers.Main + override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined +} diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java index 0817c0ab78..84b4a36399 100644 --- a/src/com/android/launcher3/util/window/WindowManagerProxy.java +++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java @@ -32,7 +32,6 @@ import static com.android.launcher3.util.RotationUtils.deltaRotation; import static com.android.launcher3.util.RotationUtils.rotateRect; import static com.android.launcher3.util.RotationUtils.rotateSize; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -40,7 +39,6 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; -import android.os.Build; import android.util.ArrayMap; import android.util.Log; import android.view.Display; @@ -54,7 +52,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.testing.shared.ResourceUtils; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.NavigationMode; @@ -121,6 +118,20 @@ public class WindowManagerProxy implements ResourceBasedOverride, SafeCloseable return false; } + /** + * Returns if the pinned taskbar should be shown when home is visible. + */ + public boolean showLockedTaskbarOnHome(Context displayInfoContext) { + return false; + } + + /** + * Returns if the home is visible. + */ + public boolean isHomeVisible(Context context) { + return false; + } + /** * Returns the real bounds for the provided display after applying any insets normalization */ @@ -216,7 +227,7 @@ public class WindowManagerProxy implements ResourceBasedOverride, SafeCloseable int screenWidthPx, @NonNull WindowInsets windowInsets, @NonNull WindowInsets.Builder insetsBuilder) { - if (!isLargeScreen || !Utilities.ATLEAST_S) { + if (!isLargeScreen) { return; } @@ -391,25 +402,16 @@ public class WindowManagerProxy implements ResourceBasedOverride, SafeCloseable /** * Returns a CachedDisplayInfo initialized for the current display */ - @TargetApi(Build.VERSION_CODES.S) public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) { int rotation = getRotation(displayInfoContext); - if (Utilities.ATLEAST_S) { - WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) - .getMaximumWindowMetrics(); - return getDisplayInfo(windowMetrics, rotation); - } else { - Point size = new Point(); - Display display = getDisplay(displayInfoContext); - display.getRealSize(size); - return new CachedDisplayInfo(size, rotation); - } + WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) + .getMaximumWindowMetrics(); + return getDisplayInfo(windowMetrics, rotation); } /** * Returns a CachedDisplayInfo initialized for the current display */ - @TargetApi(Build.VERSION_CODES.S) protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) { Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom); return new CachedDisplayInfo(size, rotation, @@ -478,8 +480,7 @@ public class WindowManagerProxy implements ResourceBasedOverride, SafeCloseable } } } - return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : - NavigationMode.THREE_BUTTONS; + return NavigationMode.NO_BUTTON; } @Override diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index cfac91a813..d3160e0db3 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -81,6 +81,7 @@ import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.ViewCache; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; import java.util.List; @@ -266,6 +267,14 @@ public interface ActivityContext { return null; } + /** + * Returns the {@link WidgetPickerDataProvider} that can be used to read widgets for display. + */ + @Nullable + default WidgetPickerDataProvider getWidgetPickerDataProvider() { + return null; + } + @Nullable default StringCache getStringCache() { return null; diff --git a/src/com/android/launcher3/views/BubbleTextHolder.java b/src/com/android/launcher3/views/BubbleTextHolder.java index 84f8049ded..d2ae93b5be 100644 --- a/src/com/android/launcher3/views/BubbleTextHolder.java +++ b/src/com/android/launcher3/views/BubbleTextHolder.java @@ -20,7 +20,7 @@ import com.android.launcher3.BubbleTextView; /** * Views that contain {@link BubbleTextView} should implement this interface. */ -public interface BubbleTextHolder extends IconLabelDotView { +public interface BubbleTextHolder extends FloatingIconViewCompanion { BubbleTextView getBubbleText(); @Override diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java index bc66a33b0c..ef66ffe091 100644 --- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java +++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java @@ -19,11 +19,19 @@ package com.android.launcher3.views; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ImageSpan; import android.util.AttributeSet; -import android.widget.TextView; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; @@ -45,22 +53,65 @@ public class DoubleShadowBubbleTextView extends BubbleTextView { public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mShadowInfo = new ShadowInfo(context, attrs, defStyle); - setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor); + mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle); + setShadowLayer( + mShadowInfo.getAmbientShadowBlur(), + 0, + 0, + mShadowInfo.getAmbientShadowColor() + ); + } + + @Override + public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) { + Drawable drawable = getContext().getDrawable(drawableId); + if (drawable == null) { + setText(text); + Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources" + + ", will just set text instead."); + return; + } + drawable.setTint(getCurrentTextColor()); + int textSize = Math.round(getTextSize()); + ImageSpan imageSpan; + if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + drawable = getDoubleShadowDrawable(drawable, textSize); + } + drawable.setBounds(0, 0, textSize, textSize); + imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER); + // First space will be replaced with Drawable, second space is for space before text. + SpannableString spannable = new SpannableString(" " + text); + spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + setText(spannable); + } + + @RequiresApi(Build.VERSION_CODES.S) + private DoubleShadowIconDrawable getDoubleShadowDrawable( + @NonNull Drawable drawable, int textSize + ) { + // add some padding via inset to avoid shadow clipping + int iconInsetSize = getContext().getResources() + .getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset); + return new DoubleShadowIconDrawable( + mShadowInfo, + drawable, + textSize, + iconInsetSize + ); } @Override public void onDraw(Canvas canvas) { // If text is transparent or shadow alpha is 0, don't draw any shadow - if (mShadowInfo.skipDoubleShadow(this)) { + if (skipDoubleShadow()) { super.onDraw(canvas); return; } int alpha = Color.alpha(getCurrentTextColor()); // We enhance the shadow by drawing the shadow twice - getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, - getTextShadowColor(mShadowInfo.ambientShadowColor, alpha)); + getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0, + getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha)); drawWithoutDot(canvas); canvas.save(); @@ -69,10 +120,10 @@ public class DoubleShadowBubbleTextView extends BubbleTextView { getScrollY() + getHeight()); getPaint().setShadowLayer( - mShadowInfo.keyShadowBlur, - mShadowInfo.keyShadowOffsetX, - mShadowInfo.keyShadowOffsetY, - getTextShadowColor(mShadowInfo.keyShadowColor, alpha)); + mShadowInfo.getKeyShadowBlur(), + mShadowInfo.getKeyShadowOffsetX(), + mShadowInfo.getKeyShadowOffsetY(), + getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha)); drawWithoutDot(canvas); canvas.restore(); @@ -80,55 +131,30 @@ public class DoubleShadowBubbleTextView extends BubbleTextView { drawRunningAppIndicatorIfNecessary(canvas); } - public static class ShadowInfo { - public final float ambientShadowBlur; - public final int ambientShadowColor; - - public final float keyShadowBlur; - public final float keyShadowOffsetX; - public final float keyShadowOffsetY; - public final int keyShadowColor; - - public ShadowInfo(Context c, AttributeSet attrs, int defStyle) { - - TypedArray a = c.obtainStyledAttributes( - attrs, R.styleable.ShadowInfo, defStyle, 0); - - ambientShadowBlur = a.getDimensionPixelSize( - R.styleable.ShadowInfo_ambientShadowBlur, 0); - ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0); - - keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0); - keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0); - keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0); - keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0); - a.recycle(); - } - - public boolean skipDoubleShadow(TextView textView) { - int textAlpha = Color.alpha(textView.getCurrentTextColor()); - int keyShadowAlpha = Color.alpha(keyShadowColor); - int ambientShadowAlpha = Color.alpha(ambientShadowColor); - if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) { - textView.getPaint().clearShadowLayer(); - return true; - } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) { - textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0, - getTextShadowColor(ambientShadowColor, textAlpha)); - return true; - } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) { - textView.getPaint().setShadowLayer( - keyShadowBlur, - keyShadowOffsetX, - keyShadowOffsetY, - getTextShadowColor(keyShadowColor, textAlpha)); - return true; - } else { - return false; - } + private boolean skipDoubleShadow() { + int textAlpha = Color.alpha(getCurrentTextColor()); + int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor()); + int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor()); + if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) { + getPaint().clearShadowLayer(); + return true; + } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) { + getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0, + getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha)); + return true; + } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) { + getPaint().setShadowLayer( + mShadowInfo.getKeyShadowBlur(), + mShadowInfo.getKeyShadowOffsetX(), + mShadowInfo.getKeyShadowOffsetY(), + getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha)); + return true; + } else { + return false; } } + // Multiplies the alpha of shadowColor by textAlpha. private static int getTextShadowColor(int shadowColor, int textAlpha) { return setColorAlphaBound(shadowColor, diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt new file mode 100644 index 0000000000..7ac7c94ae4 --- /dev/null +++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt @@ -0,0 +1,132 @@ +/* + * 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.views + +import android.content.res.ColorStateList +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.RenderEffect +import android.graphics.RenderNode +import android.graphics.Shader +import android.graphics.drawable.Drawable +import android.graphics.drawable.InsetDrawable +import android.os.Build.VERSION_CODES +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting + +/** + * Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with + * [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons. + */ +@RequiresApi(VERSION_CODES.S) +class DoubleShadowIconDrawable( + private val shadowInfo: ShadowInfo, + iconDrawable: Drawable, + private val iconSize: Int, + iconInsetSize: Int +) : Drawable() { + private val mIconDrawable: InsetDrawable + private val mDoubleShadowNode: RenderNode? + + init { + mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize) + mIconDrawable.setBounds(0, 0, iconSize, iconSize) + mDoubleShadowNode = createShadowRenderNode() + } + + @VisibleForTesting + fun createShadowRenderNode(): RenderNode { + val renderNode = RenderNode("DoubleShadowNode") + renderNode.setPosition(0, 0, iconSize, iconSize) + // Create render effects + val ambientShadow = + createShadowRenderEffect( + shadowInfo.ambientShadowBlur, + 0f, + 0f, + Color.alpha(shadowInfo.ambientShadowColor).toFloat() + ) + val keyShadow = + createShadowRenderEffect( + shadowInfo.keyShadowBlur, + shadowInfo.keyShadowOffsetX, + shadowInfo.keyShadowOffsetY, + Color.alpha(shadowInfo.keyShadowColor).toFloat() + ) + val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP) + renderNode.setRenderEffect(blend) + return renderNode + } + + @VisibleForTesting + fun createShadowRenderEffect( + radius: Float, + offsetX: Float, + offsetY: Float, + alpha: Float + ): RenderEffect { + return RenderEffect.createColorFilterEffect( + PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY), + RenderEffect.createOffsetEffect( + offsetX, + offsetY, + RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP) + ) + ) + } + + override fun draw(canvas: Canvas) { + if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) { + if (!mDoubleShadowNode.hasDisplayList()) { + // Record render node if its display list is not recorded or discarded + // (which happens when it's no longer drawn by anything). + val recordingCanvas = mDoubleShadowNode.beginRecording() + mIconDrawable.draw(recordingCanvas) + mDoubleShadowNode.endRecording() + } + canvas.drawRenderNode(mDoubleShadowNode) + } + mIconDrawable.draw(canvas) + } + + override fun getIntrinsicHeight() = iconSize + + override fun getIntrinsicWidth() = iconSize + + override fun getOpacity() = PixelFormat.TRANSPARENT + + override fun setAlpha(alpha: Int) { + mIconDrawable.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + mIconDrawable.colorFilter = colorFilter + } + + override fun setTint(color: Int) { + mIconDrawable.setTint(color) + } + + override fun setTintList(tint: ColorStateList?) { + mIconDrawable.setTintList(tint) + } +} diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java index 1e577bec36..6739387cf9 100644 --- a/src/com/android/launcher3/views/FloatingIconView.java +++ b/src/com/android/launcher3/views/FloatingIconView.java @@ -22,7 +22,7 @@ import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; import static com.android.launcher3.Utilities.getFullDrawable; import static com.android.launcher3.Utilities.mapToRange; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; -import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; +import static com.android.launcher3.views.FloatingIconViewCompanion.setPropertiesVisible; import android.animation.Animator; import android.content.Context; @@ -175,8 +175,9 @@ public class FloatingIconView extends FrameLayout implements mLauncher.getDeviceProfile(), taskViewDrawAlpha); if (mFadeOutView != null) { - // The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively. - mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR))); + // The alpha goes from 1 to 0 when progress is 0 and 0.15 respectively. + // This value minimizes view display time while still allowing the view to fade out. + mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.15f, 0, 1, LINEAR))); } } @@ -252,11 +253,14 @@ public class FloatingIconView extends FrameLayout implements public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect, Rect outViewBounds) { boolean ignoreTransform = !isOpening; - if (v instanceof BubbleTextHolder) { - v = ((BubbleTextHolder) v).getBubbleText(); + if (v instanceof DeepShortcutView dsv) { + v = dsv.getIconView(); ignoreTransform = false; - } else if (v.getParent() instanceof DeepShortcutView) { - v = ((DeepShortcutView) v.getParent()).getIconView(); + } else if (v.getParent() instanceof DeepShortcutView dsv) { + v = dsv.getIconView(); + ignoreTransform = false; + } else if (v instanceof BubbleTextHolder bth) { + v = bth.getBubbleText(); ignoreTransform = false; } if (v == null) { @@ -297,10 +301,10 @@ public class FloatingIconView extends FrameLayout implements Drawable badge = null; if (info instanceof SystemShortcut) { - if (originalView instanceof ImageView) { - drawable = ((ImageView) originalView).getDrawable(); - } else if (originalView instanceof DeepShortcutView) { - drawable = ((DeepShortcutView) originalView).getIconView().getBackground(); + if (originalView instanceof ImageView iv) { + drawable = iv.getDrawable(); + } else if (originalView instanceof DeepShortcutView dsv) { + drawable = dsv.getIconView().getBackground(); } else { drawable = originalView.getBackground(); } @@ -515,6 +519,10 @@ public class FloatingIconView extends FrameLayout implements // When closing an app, we want the item on the workspace to be invisible immediately updateViewsVisibility(false /* isVisible */); } + if (mFadeOutView instanceof FloatingIconViewCompanion fivc) { + fivc.setForceHideDot(true); + fivc.setForceHideRing(true); + } } @Override @@ -652,6 +660,10 @@ public class FloatingIconView extends FrameLayout implements if (view.mFadeOutView != null) { view.mFadeOutView.setAlpha(1f); } + if (view.mFadeOutView instanceof FloatingIconViewCompanion fivc) { + fivc.setForceHideDot(false); + fivc.setForceHideRing(false); + } if (hideOriginal) { view.updateViewsVisibility(true /* isVisible */); @@ -673,10 +685,10 @@ public class FloatingIconView extends FrameLayout implements private void updateViewsVisibility(boolean isVisible) { if (mOriginalIcon != null) { - setIconAndDotVisible(mOriginalIcon, isVisible); + setPropertiesVisible(mOriginalIcon, isVisible); } if (mMatchVisibilityView != null) { - setIconAndDotVisible(mMatchVisibilityView, isVisible); + setPropertiesVisible(mMatchVisibilityView, isVisible); } } diff --git a/src/com/android/launcher3/views/IconLabelDotView.java b/src/com/android/launcher3/views/FloatingIconViewCompanion.java similarity index 56% rename from src/com/android/launcher3/views/IconLabelDotView.java rename to src/com/android/launcher3/views/FloatingIconViewCompanion.java index e9113cf874..fc23903662 100644 --- a/src/com/android/launcher3/views/IconLabelDotView.java +++ b/src/com/android/launcher3/views/FloatingIconViewCompanion.java @@ -18,19 +18,24 @@ package com.android.launcher3.views; import android.view.View; /** - * A view that has an icon, label, and notification dot. + * A view that can be drawn (in some capacity) via) {@link FloatingIconView}. + * This interface allows us to hide certain properties of the view that the FloatingIconView + * cannot draw, which allows us to make a seamless handoff between the FloatingIconView and + * the companion view. */ -public interface IconLabelDotView { +public interface FloatingIconViewCompanion { void setIconVisible(boolean visible); void setForceHideDot(boolean hide); + default void setForceHideRing(boolean hide) {} /** * Sets the visibility of icon and dot of the view */ - static void setIconAndDotVisible(View view, boolean visible) { - if (view instanceof IconLabelDotView) { - ((IconLabelDotView) view).setIconVisible(visible); - ((IconLabelDotView) view).setForceHideDot(!visible); + static void setPropertiesVisible(View view, boolean visible) { + if (view instanceof FloatingIconViewCompanion) { + ((FloatingIconViewCompanion) view).setIconVisible(visible); + ((FloatingIconViewCompanion) view).setForceHideDot(!visible); + ((FloatingIconViewCompanion) view).setForceHideRing(!visible); } else { view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java index cab798215c..5f8e2c0973 100644 --- a/src/com/android/launcher3/views/FloatingSurfaceView.java +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -15,9 +15,8 @@ */ package com.android.launcher3.views; -import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView; -import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; +import static com.android.launcher3.views.FloatingIconViewCompanion.setPropertiesVisible; import android.content.Context; import android.graphics.Canvas; @@ -160,7 +159,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements if (mContract == null) { return; } - View icon = mLauncher.getFirstMatchForAppClose(NO_MATCHING_ID, + View icon = mLauncher.getFirstMatchForAppClose(null /* StableViewInfo */, mContract.componentName.getPackageName(), mContract.user, false /* supportsAllAppsState */); @@ -237,7 +236,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements private void setCurrentIconVisible(boolean isVisible) { if (mIcon != null) { - setIconAndDotVisible(mIcon, isVisible); + setPropertiesVisible(mIcon, isVisible); } } } diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index 62eed5c545..82cc40d93b 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -15,8 +15,6 @@ */ package com.android.launcher3.views; -import static androidx.core.content.ContextCompat.getColorStateList; - import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE; @@ -147,8 +145,7 @@ public class OptionsPopupView extends Arrow @Override public void assignMarginsAndBackgrounds(ViewGroup viewGroup) { - assignMarginsAndBackgrounds(viewGroup, - getColorStateList(getContext(), mColorIds[0]).getDefaultColor()); + assignMarginsAndBackgrounds(viewGroup, mColors[0]); // last shortcut doesn't need bottom margin final int count = viewGroup.getChildCount() - 1; for (int i = 0; i < count; i++) { diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java index fa17b7bcfe..6fd18beb1c 100644 --- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -20,6 +20,9 @@ import static android.view.HapticFeedbackConstants.CLOCK_TICK; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; +import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER; +import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER; + import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; @@ -40,11 +43,15 @@ import android.view.ViewConfiguration; import android.view.WindowInsets; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.FastScrollRecyclerView; +import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.LetterListTextView; import com.android.launcher3.graphics.FastScrollThumbDrawable; import com.android.launcher3.util.Themes; @@ -55,6 +62,19 @@ import java.util.List; * The track and scrollbar that shows when you scroll the list. */ public class RecyclerViewFastScroller extends View { + + /** FastScrollerLocation describes what RecyclerView the fast scroller is dedicated to. */ + public enum FastScrollerLocation { + UNKNOWN_SCROLLER(0), + ALL_APPS_SCROLLER(1), + WIDGET_SCROLLER(2); + + public final int location; + + FastScrollerLocation(int location) { + this.location = location; + } + } private static final String TAG = "RecyclerViewFastScroller"; private static final boolean DEBUG = false; private static final int FASTSCROLL_THRESHOLD_MILLIS = 40; @@ -106,6 +126,8 @@ public class RecyclerViewFastScroller extends View { private final Point mThumbDrawOffset = new Point(); private final Paint mTrackPaint; + private final int mThumbColor; + private final int mThumbLetterScrollerColor; private float mLastTouchY; private boolean mIsDragging; @@ -139,6 +161,7 @@ public class RecyclerViewFastScroller extends View { private int mDownX; private int mDownY; private int mLastY; + private FastScrollerLocation mFastScrollerLocation; public RecyclerViewFastScroller(Context context) { this(context, null); @@ -151,13 +174,16 @@ public class RecyclerViewFastScroller extends View { public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + mFastScrollerLocation = FastScrollerLocation.UNKNOWN_SCROLLER; mTrackPaint = new Paint(); mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); mTrackPaint.setAlpha(MAX_TRACK_ALPHA); + mThumbColor = Themes.getColorAccent(context); + mThumbLetterScrollerColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright); mThumbPaint = new Paint(); mThumbPaint.setAntiAlias(true); - mThumbPaint.setColor(Themes.getColorAccent(context)); + mThumbPaint.setColor(mThumbColor); mThumbPaint.setStyle(Paint.Style.FILL); Resources res = getResources(); @@ -329,11 +355,26 @@ public class RecyclerViewFastScroller extends View { if (!sectionName.equals(mPopupSectionName)) { mPopupSectionName = sectionName; mPopupView.setText(sectionName); - performHapticFeedback(CLOCK_TICK); + // AllApps haptics are taken care of by AllAppsFastScrollHelper. + if (mFastScrollerLocation != ALL_APPS_SCROLLER) { + performHapticFeedback(CLOCK_TICK); + } } animatePopupVisibility(!TextUtils.isEmpty(sectionName)); mLastTouchY = boundedY; setThumbOffsetY((int) mLastTouchY); + updateFastScrollerLetterList(y); + } + + private void updateFastScrollerLetterList(int y) { + if (!shouldUseLetterFastScroller()) { + return; + } + ConstraintLayout mLetterList = mRv.getLetterList(); + for (int i = 0; i < mLetterList.getChildCount(); i++) { + LetterListTextView currentLetter = (LetterListTextView) mLetterList.getChildAt(i); + currentLetter.animateBasedOnYPosition(y + mTouchOffsetY); + } } /** End any active fast scrolling touch handling, if applicable. */ @@ -359,15 +400,35 @@ public class RecyclerViewFastScroller extends View { mThumbDrawOffset.set(getWidth() / 2, mRv.getScrollBarTop()); // Draw the track float halfW = mWidth / 2; - canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(), - mWidth, mWidth, mTrackPaint); - - canvas.translate(0, mThumbOffsetY); + boolean useLetterFastScroller = shouldUseLetterFastScroller(); + if (useLetterFastScroller) { + float translateX; + if (mIsDragging) { + // halfW * 3 is half circle. + translateX = halfW * 3; + } else { + translateX = halfW * 5; + } + canvas.translate(translateX, mThumbOffsetY); + } else { + canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(), + mWidth, mWidth, mTrackPaint); + canvas.translate(0, mThumbOffsetY); + } mThumbDrawOffset.y += mThumbOffsetY; + + /* Draw half circle */ halfW += mThumbPadding; float r = getScrollThumbRadius(); - mThumbBounds.set(-halfW, 0, halfW, mThumbHeight); - canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint); + if (useLetterFastScroller) { + mThumbPaint.setColor(mThumbLetterScrollerColor); + mThumbBounds.set(0, 0, 0, mThumbHeight); + canvas.drawCircle(-halfW, halfW, r * 2, mThumbPaint); + } else { + mThumbPaint.setColor(mThumbColor); + mThumbBounds.set(-halfW, 0, halfW, mThumbHeight); + canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint); + } mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0)); // swiping very close to the thumb area (not just within it's bound) // will also prevent back gesture @@ -380,6 +441,11 @@ public class RecyclerViewFastScroller extends View { canvas.restoreToCount(saveCount); } + boolean shouldUseLetterFastScroller() { + return Flags.letterFastScroller() + && getScrollerLocation() == FastScrollerLocation.ALL_APPS_SCROLLER; + } + @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { mSystemGestureInsets = insets.getSystemGestureInsets(); @@ -421,19 +487,25 @@ public class RecyclerViewFastScroller extends View { return isNearThumb(x, y); } - /** - * Returns whether the specified x position is near the scroll bar. - */ - public boolean isNearScrollBar(int x) { - return x >= (getWidth() - mMaxWidth) / 2 - mScrollbarLeftOffsetTouchDelegate - && x <= (getWidth() + mMaxWidth) / 2; + public FastScrollerLocation getScrollerLocation() { + return mFastScrollerLocation; + } + + public void setFastScrollerLocation(@NonNull FastScrollerLocation location) { + mFastScrollerLocation = location; } private void animatePopupVisibility(boolean visible) { if (mPopupVisible != visible) { mPopupVisible = visible; - mPopupView.animate().cancel(); - mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start(); + if (shouldUseLetterFastScroller()) { + mRv.getLetterList().animate().alpha(visible ? 1f : 0f) + .setDuration(visible ? 200 : 150).start(); + } else { + mPopupView.animate().cancel(); + mPopupView.animate().alpha(visible ? 1f : 0f) + .setDuration(visible ? 200 : 150).start(); + } } } diff --git a/src/com/android/launcher3/views/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt new file mode 100644 index 0000000000..4f626ec520 --- /dev/null +++ b/src/com/android/launcher3/views/ShadowInfo.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.views + +import android.content.Context +import android.util.AttributeSet +import com.android.launcher3.R + +/** + * Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for + * "double shadow" effect. + */ +data class ShadowInfo( + val ambientShadowBlur: Float, + val ambientShadowColor: Int, + val keyShadowBlur: Float, + val keyShadowOffsetX: Float, + val keyShadowOffsetY: Float, + val keyShadowColor: Int +) { + + companion object { + /** Constructs instance of ShadowInfo from Context and given attribute set. */ + @JvmStatic + fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo { + val styledAttrs = + context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0) + val shadowInfo = + ShadowInfo( + ambientShadowBlur = + styledAttrs + .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0) + .toFloat(), + ambientShadowColor = + styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0), + keyShadowBlur = + styledAttrs + .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0) + .toFloat(), + keyShadowOffsetX = + styledAttrs + .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0) + .toFloat(), + keyShadowOffsetY = + styledAttrs + .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0) + .toFloat(), + keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0) + ) + styledAttrs.recycle() + return shadowInfo + } + } +} diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java index 923eb19d3b..a13152ec76 100644 --- a/src/com/android/launcher3/views/SpringRelativeLayout.java +++ b/src/com/android/launcher3/views/SpringRelativeLayout.java @@ -25,8 +25,6 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory; -import com.android.launcher3.Utilities; - /** * View group to allow rendering overscroll effect in a child at the parent level */ @@ -46,10 +44,8 @@ public class SpringRelativeLayout extends RelativeLayout { public SpringRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mEdgeGlowTop = Utilities.ATLEAST_S - ? new EdgeEffect(context, attrs) : new EdgeEffect(context); - mEdgeGlowBottom = Utilities.ATLEAST_S - ? new EdgeEffect(context, attrs) : new EdgeEffect(context); + mEdgeGlowTop = new EdgeEffect(context, attrs); + mEdgeGlowBottom = new EdgeEffect(context, attrs); setWillNotDraw(false); } diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java index 104209ef53..12a14c2d0e 100644 --- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java @@ -104,13 +104,13 @@ public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHo @UiThread private void enforceRoundedCorners() { - if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) { + if (mEnforcedCornerRadius <= 0) { resetRoundedCorners(); return; } View background = RoundedCornerEnforcement.findBackground(this); if (background == null - || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { + || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) { resetRoundedCorners(); return; } diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java index 13680846f9..1c0d94cbf4 100644 --- a/src/com/android/launcher3/widget/BaseWidgetSheet.java +++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java @@ -45,13 +45,13 @@ import com.android.launcher3.Launcher; import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.R; import com.android.launcher3.model.WidgetItem; -import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.Themes; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.views.AbstractSlideInView; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener; import java.util.concurrent.atomic.AtomicBoolean; @@ -60,7 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public abstract class BaseWidgetSheet extends AbstractSlideInView implements OnClickListener, OnLongClickListener, - PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener { + WidgetPickerDataChangeListener, Insettable, OnDeviceProfileChangeListener { /** The default number of cells that can fit horizontally in a widget sheet. */ public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4; @@ -106,14 +106,14 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext()) .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect()); mNavBarScrimHeight = getNavBarScrimHeight(windowInsets); - mActivityContext.getPopupDataProvider().setChangeListener(this); + mActivityContext.getWidgetPickerDataProvider().setChangeListener(this); mActivityContext.addOnDeviceProfileChangeListener(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mActivityContext.getPopupDataProvider().setChangeListener(null); + mActivityContext.getWidgetPickerDataProvider().setChangeListener(null); mActivityContext.removeOnDeviceProfileChangeListener(this); } @@ -331,8 +331,21 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView * status bar, into account. */ protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthUsed = getInsetsWidth(); + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + measureChildWithMargins(mContent, widthMeasureSpec, + widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec)); + } + + /** + * Returns the width used on left and right by the insets / padding. + */ + protected int getInsetsWidth() { int widthUsed; + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); if (deviceProfile.isTablet) { widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile), 2 * (mInsets.left + mInsets.right)); @@ -343,11 +356,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView widthUsed = Math.max(padding.left + padding.right, 2 * (mInsets.left + mInsets.right)); } - - measureChildWithMargins(mContent, widthMeasureSpec, - widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), - MeasureSpec.getSize(heightMeasureSpec)); + return widthUsed; } /** Returns the horizontal margins to be applied to the widget sheet. **/ diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java index 2817299d4a..ab42839e20 100644 --- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java +++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java @@ -183,19 +183,14 @@ public class DatabaseWidgetPreviewLoader { // Draw horizontal and vertical lines to represent individual columns. final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); + boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */ + previewWidthF, /* bottom= */ previewHeightF); - if (Utilities.ATLEAST_S) { - boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */ - previewWidthF, /* bottom= */ previewHeightF); - - p.setStyle(Paint.Style.FILL); - p.setColor(Color.WHITE); - float roundedCorner = mContext.getResources().getDimension( - android.R.dimen.system_app_widget_background_radius); - c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p); - } else { - boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF); - } + p.setStyle(Paint.Style.FILL); + p.setColor(Color.WHITE); + float roundedCorner = mContext.getResources().getDimension( + android.R.dimen.system_app_widget_background_radius); + c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p); p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(mContext.getResources() diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java index 40c39840d6..91b899c2ef 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java @@ -21,22 +21,17 @@ import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_I import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.LauncherAppState; import com.android.launcher3.util.Executors; -import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.WeakHashMap; import java.util.function.IntConsumer; /** @@ -83,6 +78,11 @@ class LauncherAppWidgetHost extends AppWidgetHost { mViewToRecycle = viewToRecycle; } + @VisibleForTesting + @Nullable ListenableHostView getViewToRecycle() { + return mViewToRecycle; + } + @Override @NonNull public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId, @@ -129,37 +129,4 @@ class LauncherAppWidgetHost extends AppWidgetHost { public void clearViews() { super.clearViews(); } - - public static class ListenableHostView extends LauncherAppWidgetHostView { - - private Set mUpdateListeners = Collections.EMPTY_SET; - - ListenableHostView(Context context) { - super(context); - } - - @Override - public void updateAppWidget(RemoteViews remoteViews) { - super.updateAppWidget(remoteViews); - mUpdateListeners.forEach(Runnable::run); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(LauncherAppWidgetHostView.class.getName()); - } - - /** - * Adds a callback to be run everytime the provided app widget updates. - * @return a closable to remove this callback - */ - public SafeCloseable addUpdateListener(Runnable callback) { - if (mUpdateListeners == Collections.EMPTY_SET) { - mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>()); - } - mUpdateListeners.add(callback); - return () -> mUpdateListeners.remove(callback); - } - } } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java index 3e4fd8caa8..e77ba249bd 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java @@ -1,7 +1,5 @@ package com.android.launcher3.widget; -import static com.android.launcher3.Utilities.ATLEAST_S; - import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; @@ -116,15 +114,13 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo getSpanY(widgetPadding, minResizeHeight, dp.cellLayoutBorderSpacePx.y, cellSize.y)); - if (ATLEAST_S) { - if (maxResizeWidth > 0) { - maxSpanX = Math.min(maxSpanX, getSpanX(widgetPadding, maxResizeWidth, - dp.cellLayoutBorderSpacePx.x, cellSize.x)); - } - if (maxResizeHeight > 0) { - maxSpanY = Math.min(maxSpanY, getSpanY(widgetPadding, maxResizeHeight, - dp.cellLayoutBorderSpacePx.y, cellSize.y)); - } + if (maxResizeWidth > 0) { + maxSpanX = Math.min(maxSpanX, getSpanX(widgetPadding, maxResizeWidth, + dp.cellLayoutBorderSpacePx.x, cellSize.x)); + } + if (maxResizeHeight > 0) { + maxSpanY = Math.min(maxSpanY, getSpanY(widgetPadding, maxResizeHeight, + dp.cellLayoutBorderSpacePx.y, cellSize.y)); } spanX = Math.max(spanX, @@ -135,18 +131,16 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo cellSize.y)); } - if (ATLEAST_S) { - // Ensures maxSpan >= minSpan - maxSpanX = Math.max(maxSpanX, minSpanX); - maxSpanY = Math.max(maxSpanY, minSpanY); + // Ensures maxSpan >= minSpan + maxSpanX = Math.max(maxSpanX, minSpanX); + maxSpanY = Math.max(maxSpanY, minSpanY); - // Use targetCellWidth/Height if it is within the min/max ranges. - // Otherwise, use the span of minWidth/Height. - if (targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX - && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) { - spanX = targetCellWidth; - spanY = targetCellHeight; - } + // Use targetCellWidth/Height if it is within the min/max ranges. + // Otherwise, use the span of minWidth/Height. + if (targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX + && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) { + spanX = targetCellWidth; + spanY = targetCellHeight; } // If minSpanX/Y > spanX/Y, ignore the minSpanX/Y to match the behavior described in @@ -213,8 +207,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo } public boolean isConfigurationOptional() { - return ATLEAST_S - && isReconfigurable() + return isReconfigurable() && (getWidgetFeatures() & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0; } diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java index a5e22c5899..f499fca165 100644 --- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java +++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java @@ -20,6 +20,7 @@ import static android.app.Activity.RESULT_CANCELED; import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; import static com.android.launcher3.Flags.enableWorkspaceInflation; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; import android.appwidget.AppWidgetHost; @@ -36,6 +37,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; @@ -44,13 +47,14 @@ import com.android.launcher3.Utilities; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.ResourceBasedOverride; import com.android.launcher3.util.SafeCloseable; -import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView; import com.android.launcher3.widget.custom.CustomWidgetManager; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntConsumer; /** @@ -77,7 +81,7 @@ public class LauncherWidgetHolder { protected final SparseArray mViews = new SparseArray<>(); protected final List mProviderChangedListeners = new ArrayList<>(); - protected int mFlags = FLAG_STATE_IS_NORMAL; + protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL); // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"; @@ -96,6 +100,10 @@ public class LauncherWidgetHolder { context, appWidgetRemovedCallback, mProviderChangedListeners); } + protected LooperExecutor getWidgetHolderExecutor() { + return UI_HELPER_EXECUTOR; + } + /** * Starts listening to the widget updates from the server side */ @@ -104,21 +112,23 @@ public class LauncherWidgetHolder { return; } - try { - mWidgetHost.startListening(); - } catch (Exception e) { - if (!Utilities.isBinderSizeError(e)) { - throw new RuntimeException(e); + getWidgetHolderExecutor().execute(() -> { + try { + mWidgetHost.startListening(); + } catch (Exception e) { + if (!Utilities.isBinderSizeError(e)) { + throw new RuntimeException(e); + } + // We're willing to let this slide. The exception is being caused by the list of + // RemoteViews which is being passed back. The startListening relationship will + // have been established by this point, and we will end up populating the + // widgets upon bind anyway. See issue 14255011 for more context. } - // We're willing to let this slide. The exception is being caused by the list of - // RemoteViews which is being passed back. The startListening relationship will - // have been established by this point, and we will end up populating the - // widgets upon bind anyway. See issue 14255011 for more context. - } - // TODO: Investigate why widgetHost.startListening() always return non-empty updates - setListeningFlag(true); + // TODO: Investigate why widgetHost.startListening() always return non-empty updates + setListeningFlag(true); - updateDeferredView(); + MAIN_EXECUTOR.execute(() -> updateDeferredView()); + }); } /** @@ -282,16 +292,23 @@ public class LauncherWidgetHolder { if (!WIDGETS_ENABLED) { return; } - mWidgetHost.stopListening(); - setListeningFlag(false); + getWidgetHolderExecutor().execute(() -> { + mWidgetHost.stopListening(); + setListeningFlag(false); + }); } + /** + * Update {@link FLAG_LISTENING} on {@link mFlags} after making binder calls from + * {@link sWidgetHost}. + */ + @WorkerThread protected void setListeningFlag(final boolean isListening) { if (isListening) { - mFlags |= FLAG_LISTENING; + mFlags.updateAndGet(old -> old | FLAG_LISTENING); return; } - mFlags &= ~FLAG_LISTENING; + mFlags.updateAndGet(old -> old & ~FLAG_LISTENING); } /** @@ -373,7 +390,7 @@ public class LauncherWidgetHolder { * as a result of using the same flow. */ protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) { - if ((mFlags & FLAG_LISTENING) == 0) { + if ((mFlags.get() & FLAG_LISTENING) == 0) { if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) { return view; } else { @@ -395,7 +412,7 @@ public class LauncherWidgetHolder { @NonNull protected LauncherAppWidgetHostView createViewInternal( int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { - if ((mFlags & FLAG_LISTENING) == 0) { + if ((mFlags.get() & FLAG_LISTENING) == 0) { // Since the launcher hasn't started listening to widget updates, we can't simply call // host.createView here because the later will make a binder call to retrieve // RemoteViews from system process. @@ -460,25 +477,27 @@ public class LauncherWidgetHolder { * @return True if the host is listening to the updates, false otherwise */ public boolean isListening() { - return (mFlags & FLAG_LISTENING) != 0; + return (mFlags.get() & FLAG_LISTENING) != 0; } /** * Sets or unsets a flag the can change whether the widget host should be in the listening * state. */ - private void setShouldListenFlag(int flag, boolean on) { + @VisibleForTesting + void setShouldListenFlag(int flag, boolean on) { if (on) { - mFlags |= flag; + mFlags.updateAndGet(old -> old | flag); } else { - mFlags &= ~flag; + mFlags.updateAndGet(old -> old & ~flag); } final boolean listening = isListening(); - if (!listening && shouldListen(mFlags)) { + int currentFlag = mFlags.get(); + if (!listening && shouldListen(currentFlag)) { // Postpone starting listening until all flags are on. startListening(); - } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) { + } else if (listening && (currentFlag & FLAG_ACTIVITY_STARTED) == 0) { // Postpone stopping listening until the activity is stopped. stopListening(); } diff --git a/src/com/android/launcher3/widget/ListenableHostView.java b/src/com/android/launcher3/widget/ListenableHostView.java new file mode 100644 index 0000000000..b809db016b --- /dev/null +++ b/src/com/android/launcher3/widget/ListenableHostView.java @@ -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.launcher3.widget; + +import android.content.Context; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.RemoteViews; + +import com.android.launcher3.util.SafeCloseable; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +public class ListenableHostView extends LauncherAppWidgetHostView { + + private Set mUpdateListeners = Collections.EMPTY_SET; + + ListenableHostView(Context context) { + super(context); + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + super.updateAppWidget(remoteViews); + mUpdateListeners.forEach(Runnable::run); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(LauncherAppWidgetHostView.class.getName()); + } + + /** + * Adds a callback to be run everytime the provided app widget updates. + * @return a closable to remove this callback + */ + public SafeCloseable addUpdateListener(Runnable callback) { + if (mUpdateListeners == Collections.EMPTY_SET) { + mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>()); + } + mUpdateListeners.add(callback); + return () -> mUpdateListeners.remove(callback); + } +} diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java index 8857774922..130d533ca4 100644 --- a/src/com/android/launcher3/widget/PendingItemDragHelper.java +++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java @@ -136,9 +136,7 @@ public class PendingItemDragHelper extends DragPreviewProvider { Drawable p = new FastBitmapDrawable(new DatabaseWidgetPreviewLoader(launcher) .generateWidgetPreview( createWidgetInfo.info, maxWidth, previewSizeBeforeScale)); - if (RoundedCornerEnforcement.isRoundedCornerEnabled()) { - p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget); - } + p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget); preview = p; } diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java index 1e46ffd30c..cadaf89ef0 100644 --- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java +++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java @@ -28,8 +28,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; import java.util.ArrayList; import java.util.List; @@ -67,15 +65,10 @@ public class RoundedCornerEnforcement { /** * Check whether the app widget has opted out of the enforcement. */ - public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) { + public static boolean hasAppWidgetOptedOut(@NonNull View background) { return background.getId() == android.R.id.background && background.getClipToOutline(); } - /** Check if the app widget is in the deny list. */ - public static boolean isRoundedCornerEnabled() { - return Utilities.ATLEAST_S && FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get(); - } - /** * Computes the rounded rectangle needed for this app widget. * @@ -102,9 +95,6 @@ public class RoundedCornerEnforcement { * in the given context. */ public static float computeEnforcedRadius(@NonNull Context context) { - if (!Utilities.ATLEAST_S) { - return 0; - } Resources res = context.getResources(); float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius); float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius); diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 35372d3af5..b7ad95e3e3 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -40,7 +40,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -499,12 +498,6 @@ public class WidgetCell extends LinearLayout { return WidgetCell.class.getName(); } - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams(); diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java index f3320548bb..352c0a3ccb 100644 --- a/src/com/android/launcher3/widget/WidgetImageView.java +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -24,6 +24,8 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import com.android.launcher3.icons.RoundDrawableWrapper; + /** * View that draws a bitmap horizontally centered. If the image width is greater than the view * width, the image is scaled down appropriately. @@ -85,6 +87,11 @@ public class WidgetImageView extends View { final float scale = bitmapAspectRatio > containerAspectRatio ? myWidth / bitmapWidth : myHeight / bitmapHeight; + // When updating the scale, also update scale on drawable if it has rounding. + if (mDrawable instanceof RoundDrawableWrapper && scale <= 1) { + ((RoundDrawableWrapper) mDrawable).setCornerRadiusScale(scale); + } + final float scaledWidth = bitmapWidth * scale; final float scaledHeight = bitmapHeight * scale; diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java index 9132b4f18c..23d05852f2 100644 --- a/src/com/android/launcher3/widget/WidgetManagerHelper.java +++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java @@ -32,6 +32,7 @@ import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -57,8 +58,13 @@ public class WidgetManagerHelper { final Context mContext; public WidgetManagerHelper(Context context) { + this(context, AppWidgetManager.getInstance(context)); + } + + @VisibleForTesting + public WidgetManagerHelper(Context context, AppWidgetManager appWidgetManager) { mContext = context; - mAppWidgetManager = AppWidgetManager.getInstance(context); + mAppWidgetManager = appWidgetManager; } /** diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index 894099de74..ddbd291f80 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -17,6 +17,7 @@ package com.android.launcher3.widget; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY; +import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser; import android.content.Context; import android.graphics.Rect; @@ -40,6 +41,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.picker.model.data.WidgetPickerData; import com.android.launcher3.widget.util.WidgetsTableUtils; import java.util.List; @@ -124,10 +126,10 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { @Override public void onWidgetsBound() { - List widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser( - new PackageUserKey( - mOriginalItemInfo.getTargetComponent().getPackageName(), - mOriginalItemInfo.user)); + final WidgetPickerData data = mActivityContext.getWidgetPickerDataProvider().get(); + final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(mOriginalItemInfo); + List widgets = packageUserKey != null ? findAllWidgetsForPackageUser(data, + packageUserKey) : List.of(); TableLayout widgetsTable = findViewById(R.id.widgets_table); widgetsTable.removeAllViews(); @@ -247,4 +249,7 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { } } } + + @Override + public void onRecommendedWidgetsBound() {} // no op } diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java index 398b1df394..5ad9222335 100644 --- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java @@ -22,6 +22,8 @@ import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; @@ -52,6 +54,9 @@ public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo } } + @VisibleForTesting + CustomAppWidgetProviderInfo() {} + @Override public void initSpans(Context context, InvariantDeviceProfile idp) { mIsMinSizeFulfilled = Math.min(spanX, minSpanX) <= idp.numColumns diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java index 50012b36f6..faa5d12875 100644 --- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java +++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java @@ -30,6 +30,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; import com.android.launcher3.util.MainThreadInitializedObject; @@ -45,6 +46,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; @@ -62,9 +64,16 @@ public class CustomWidgetManager implements PluginListener, private final HashMap mPlugins; private final List mCustomWidgets; private Consumer mWidgetRefreshCallback; + private final @NonNull AppWidgetManager mAppWidgetManager; private CustomWidgetManager(Context context) { + this(context, AppWidgetManager.getInstance(context)); + } + + @VisibleForTesting + CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) { mContext = context; + mAppWidgetManager = widgetManager; mPlugins = new HashMap<>(); mCustomWidgets = new ArrayList<>(); PluginManagerWrapper.INSTANCE.get(context) @@ -94,7 +103,7 @@ public class CustomWidgetManager implements PluginListener, @Override public void onPluginConnected(CustomWidgetPlugin plugin, Context context) { - List providers = AppWidgetManager.getInstance(context) + List providers = mAppWidgetManager .getInstalledProvidersForProfile(Process.myUserHandle()); if (providers.isEmpty()) return; Parcel parcel = Parcel.obtain(); @@ -113,6 +122,12 @@ public class CustomWidgetManager implements PluginListener, mCustomWidgets.removeIf(w -> w.getComponent().equals(cn)); } + @VisibleForTesting + @NonNull + Map getPlugins() { + return mPlugins; + } + /** * Inject a callback function to refresh the widgets. */ diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt new file mode 100644 index 0000000000..1abe4da98b --- /dev/null +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget.model + +import android.content.Context +import com.android.launcher3.compat.AlphabeticIndexCompat +import com.android.launcher3.model.WidgetItem +import com.android.launcher3.model.data.PackageItemInfo +import java.util.function.Predicate + +/** + * A helper class that builds the list of [WidgetsListBaseEntry]s used by the UI to display widgets. + */ +class WidgetsListBaseEntriesBuilder(val context: Context) { + + /** Builds the widgets list entries in a format understandable by the widget picking UI. */ + @JvmOverloads + fun build( + widgetsByPackageItem: Map>, + widgetFilter: Predicate = Predicate { true }, + ): List { + val indexer = AlphabeticIndexCompat(context) + + return buildList { + for ((pkgItem, widgetItems) in widgetsByPackageItem.entries) { + val filteredWidgetItems = widgetItems.filter { widgetFilter.test(it) } + if (filteredWidgetItems.isNotEmpty()) { + // Enables fast scroll popup to show right characters in all locales. + val sectionName = pkgItem.title?.let { indexer.computeSectionName(it) } ?: "" + + add(WidgetsListHeaderEntry.create(pkgItem, sectionName, filteredWidgetItems)) + add(WidgetsListContentEntry(pkgItem, sectionName, filteredWidgetItems)) + } + } + } + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java index 072d1d5948..a68effd101 100644 --- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java @@ -19,6 +19,8 @@ package com.android.launcher3.widget.picker; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import com.android.launcher3.R; + import java.util.Objects; /** @@ -26,6 +28,10 @@ import java.util.Objects; * option in the pop-up opened on long press of launcher workspace). */ public class WidgetRecommendationCategory implements Comparable { + public static WidgetRecommendationCategory DEFAULT_WIDGET_RECOMMENDATION_CATEGORY = + new WidgetRecommendationCategory( + R.string.others_widget_recommendation_category_label, /*order=*/0); + /** Resource id that holds the user friendly label for the category. */ @StringRes public final int categoryTitleRes; diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java index 9260af9b22..d84a2195d8 100644 --- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java @@ -16,7 +16,7 @@ package com.android.launcher3.widget.picker; -import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering; +import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering; import android.content.ComponentName; import android.content.Context; @@ -38,6 +38,7 @@ import com.android.launcher3.model.WidgetItem; import com.android.launcher3.pageindicators.PageIndicatorDots; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -57,6 +58,12 @@ public final class WidgetRecommendationsView extends PagedView mCategoryTitles = new ArrayList<>(); @@ -87,6 +94,14 @@ public final class WidgetRecommendationsView extends PagedView> recommendations) { + if (mShowFullPageViewIfLowDensity) { + boolean hasLessCategories = recommendations.size() < MAX_CATEGORIES; + long lowDensityCategoriesCount = recommendations.values() + .stream() + .limit(MAX_CATEGORIES) + .filter(items -> items.size() < IDEAL_ITEMS_PER_CATEGORY).count(); + + // If there less number of categories or if there are at least 2 categorizes with less + // widgets, prefer showing single page view. + return hasLessCategories || lowDensityCategoriesCount > 1; + } + return false; + } + /** * Displays the recommendations grouped by categories as pages. *

In case of a single category, no title is displayed for it.

@@ -188,6 +219,14 @@ public final class WidgetRecommendationsView extends PagedView> recommendations, DeviceProfile deviceProfile, final @Px float availableHeight, final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) { + if (shouldShowFullPageView(recommendations)) { + // Show all widgets in single page with unlimited available height. + return setRecommendations( + recommendations.values().stream().flatMap(Collection::stream).toList(), + deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth, + cellPadding); + + } this.mAvailableHeight = availableHeight; this.mAvailableWidth = availableWidth; Context context = getContext(); @@ -325,7 +364,7 @@ public final class WidgetRecommendationsView extends PagedView> rows = groupWidgetItemsUsingRowPxWithoutReordering( + List> rows = groupWidgetItemsUsingRowPxWithReordering( filteredRecommendedWidgets, context, deviceProfile, diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 9929892e28..c8ad564a7f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -20,6 +20,7 @@ import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; +import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER; import android.animation.Animator; import android.content.Context; @@ -55,7 +56,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; @@ -70,10 +70,12 @@ import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.search.SearchModeListener; import com.android.launcher3.widget.picker.search.WidgetsSearchBar; +import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider; import com.android.launcher3.workprofile.PersonalWorkPagedView; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,7 +120,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet WidgetsRecyclerView searchRecyclerView = mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; if (mIsInSearchMode && searchRecyclerView != null) { - searchRecyclerView.bindFastScrollbar(mFastScroller); + searchRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); } } @@ -136,7 +138,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet private WidgetsRecyclerView mCurrentTouchEventRecyclerView; @Nullable PersonalWorkPagedView mViewPager; - private boolean mIsInSearchMode; + protected boolean mIsInSearchMode; private boolean mIsNoWidgetsViewNeeded; @Px protected int mMaxSpanPerRow; @@ -246,8 +248,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container); mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar); - mSearchBar.initialize( - mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this); + mSearchBar.initialize(new WidgetsSearchDataProvider() { + @Override + public List getWidgets() { + return getWidgetsToDisplay(); + } + }, /* searchModeListener= */ this); } private void setDeviceManagementResources() { @@ -271,7 +277,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet } private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { - recyclerView.bindFastScrollbar(mFastScroller); + recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); if (mCurrentWidgetsRecyclerView != recyclerView) { // Only reset the scroll position & expanded apps if the currently shown recycler view // has been updated. @@ -285,10 +291,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { // The first item is always an empty space entry. Look for any more items. boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); - adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) { mNoWidgetsView.setText(R.string.no_search_results); + adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); } else if (adapterHolder.mAdapterType == AdapterHolder.WORK && mUserCache.getUserProfiles().stream() .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork()) @@ -416,19 +422,18 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + updateMaxSpansPerRow(availableWidth); doMeasure(widthMeasureSpec, heightMeasureSpec); - - if (updateMaxSpansPerRow()) { - doMeasure(widthMeasureSpec, heightMeasureSpec); - } } - /** Returns {@code true} if the max spans have been updated. */ - private boolean updateMaxSpansPerRow() { - if (getMeasuredWidth() == 0) return false; - - @Px int maxHorizontalSpan = getContentView().getMeasuredWidth() - - (2 * mContentHorizontalMargin); + /** Returns {@code true} if the max spans have been updated. + * + * @param availableWidth Total width available within parent (includes insets). + */ + private void updateMaxSpansPerRow(int availableWidth) { + @Px int maxHorizontalSpan = getAvailableWidthForSuggestions( + availableWidth - getInsetsWidth()); if (mMaxSpanPerRow != maxHorizontalSpan) { mMaxSpanPerRow = maxHorizontalSpan; mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( @@ -439,16 +444,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( maxHorizontalSpan); } - onRecommendedWidgetsBound(); - return true; + post(this::onRecommendedWidgetsBound); } - return false; } - protected View getContentView() { - return mHasWorkProfile - ? mViewPager - : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; + /** + * Returns the width available to display suggestions. + */ + protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { + return pickerAvailableWidth - (2 * mContentHorizontalMargin); } @Override @@ -465,22 +469,28 @@ public class WidgetsFullSheet extends BaseWidgetSheet setTranslationShift(mTranslationShift); } + /** + * Returns all displayable widgets. + */ + protected List getWidgetsToDisplay() { + return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets(); + } + @Override public void onWidgetsBound() { if (mIsInSearchMode) { return; } - List allWidgets = - mActivityContext.getPopupDataProvider().getAllWidgets(); + List widgets = getWidgetsToDisplay(); AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); - primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); + primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets); if (mHasWorkProfile) { mViewPager.setVisibility(VISIBLE); mTabBar.setVisibility(VISIBLE); AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); - workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); + workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets); onActivePageChanged(mViewPager.getCurrentPage()); } else { onActivePageChanged(0); @@ -493,7 +503,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet .mWidgetsListAdapter.hasVisibleEntries()); if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; - onRecommendedWidgetsBound(); + post(this::onRecommendedWidgetsBound); } } @@ -547,9 +557,11 @@ public class WidgetsFullSheet extends BaseWidgetSheet mNoWidgetsView.setVisibility(GONE); } else { mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); + mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility( + VISIBLE); // Visibility of recommended widgets, recycler views and headers are handled in methods // below. - onRecommendedWidgetsBound(); + post(this::onRecommendedWidgetsBound); onWidgetsBound(); } } @@ -564,12 +576,11 @@ public class WidgetsFullSheet extends BaseWidgetSheet if (mIsInSearchMode) { return; } - if (enableCategorizedWidgetSuggestions()) { // We avoid applying new recommendations when some are already displayed. if (mRecommendedWidgetsMap.isEmpty()) { mRecommendedWidgetsMap = - mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(); + mActivityContext.getWidgetPickerDataProvider().get().getRecommendations(); } mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( mRecommendedWidgetsMap, @@ -581,17 +592,20 @@ public class WidgetsFullSheet extends BaseWidgetSheet ); } else { if (mRecommendedWidgets.isEmpty()) { - mRecommendedWidgets = - mActivityContext.getPopupDataProvider().getRecommendedWidgets(); + mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get() + .getRecommendations() + .values().stream() + .flatMap(Collection::stream).toList(); + mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( + mRecommendedWidgets, + mDeviceProfile, + /* availableHeight= */ getMaxAvailableHeightForRecommendations(), + /* availableWidth= */ mMaxSpanPerRow, + /* cellPadding= */ mWidgetCellHorizontalPadding + ); } - mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( - mRecommendedWidgets, - mDeviceProfile, - /* availableHeight= */ getMaxAvailableHeightForRecommendations(), - /* availableWidth= */ mMaxSpanPerRow, - /* cellPadding= */ mWidgetCellHorizontalPadding - ); } + mWidgetRecommendationsContainer.setVisibility( mRecommendedWidgetsCount > 0 ? VISIBLE : GONE); } @@ -682,6 +696,18 @@ public class WidgetsFullSheet extends BaseWidgetSheet return sheet; } + /** + * Updates the widget picker's title and description in the header to the provided values (if + * present). + */ + public void mayUpdateTitleAndDescription(@Nullable String title, + @Nullable String descriptionRes) { + if (title != null) { + mHeaderTitle.setText(title); + } + // Full sheet doesn't support a description. + } + @Override public void saveHierarchyState(SparseArray sparseArray) { Bundle bundle = new Bundle(); @@ -1034,7 +1060,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mWidgetsRecyclerView.setClipToOutline(true); mWidgetsRecyclerView.setClipChildren(false); mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); - mWidgetsRecyclerView.bindFastScrollbar(mFastScroller); + mWidgetsRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator); mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); if (!isTwoPane()) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java index 854700fed3..6a1921eb23 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java @@ -16,6 +16,8 @@ package com.android.launcher3.widget.picker; +import static android.animation.ValueAnimator.areAnimatorsEnabled; + import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -26,6 +28,14 @@ public class WidgetsListItemAnimator extends DefaultItemAnimator { public static final int MOVE_DURATION_MS = 90; public static final int ADD_DURATION_MS = 120; + // DefaultItemAnimator runs change and move animations before running add animations (i.e. + // before expanded list item's content start animating to become visible on screen). + public static final int WIDGET_LIST_ITEM_APPEARANCE_START_DELAY = + areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0; + // Delay after which all item animations are ran and list item's content is visible. + public static final int WIDGET_LIST_ITEM_APPEARANCE_DELAY = + WIDGET_LIST_ITEM_APPEARANCE_START_DELAY + ADD_DURATION_MS; + public WidgetsListItemAnimator() { super(); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index 45d733a3a5..679b0f566b 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,10 +15,7 @@ */ package com.android.launcher3.widget.picker; -import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.CHANGE_DURATION_MS; -import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.MOVE_DURATION_MS; - -import static android.animation.ValueAnimator.areAnimatorsEnabled; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_START_DELAY; import android.content.Context; import android.graphics.Bitmap; @@ -157,8 +154,7 @@ public final class WidgetsListTableViewHolderBinder // Pass resize delay to let the "move" and "change" animations run before resizing the // row. tableRow.setupRow(widgetItems.size(), - /*resizeDelayMs=*/ - areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0); + /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY); if (tableRow.getChildCount() > widgetItems.size()) { for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) { tableRow.getChildAt(j).setVisibility(View.GONE); diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 1ed3d884c4..0bcab60076 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -18,7 +18,7 @@ package com.android.launcher3.widget.picker; import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx; -import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR; +import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_COUNT_COMPARATOR; import static java.lang.Math.max; @@ -163,6 +163,6 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { } // Perform re-ordering once we have filtered out recommendations that fit. - return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList(); + return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR).toList(); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 5d71db6e39..f4b99a0e81 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -21,19 +21,27 @@ import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER; import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER; import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree; import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY; +import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser; import android.content.Context; import android.graphics.Rect; import android.os.Process; +import android.os.UserHandle; import android.util.AttributeSet; +import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.ScrollView; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -58,9 +66,6 @@ import java.util.List; * Popup for showing the full list of available widgets with a two-pane layout. */ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { - - private static final int PERSONAL_TAB = 0; - private static final int WORK_TAB = 1; private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268; private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395; private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry"; @@ -81,6 +86,18 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { private int mActivePage = -1; @Nullable private PackageUserKey mSelectedHeader; + private TextView mHeaderDescription; + + /** + * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the + * picker. + */ + protected View mWidgetOptionsMenu; + /** + * State of the options in the menu (if displayed to the user). + */ + @Nullable + protected WidgetOptionsMenuState mWidgetOptionsMenuState = null; public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); @@ -118,12 +135,20 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); mWidgetRecommendationsView.setWidgetCellLongClickListener(this); mWidgetRecommendationsView.setWidgetCellOnClickListener(this); + if (!mDeviceProfile.isTwoPanels) { + mWidgetRecommendationsView.enableFullPageViewIfLowDensity(); + } // To save the currently displayed page, so that, it can be requested when rebinding // recommendations with different size constraints. mWidgetRecommendationsView.addPageSwitchListener( newPage -> mRecommendationsCurrentPage = newPage); mHeaderTitle = mContent.findViewById(R.id.title); + mHeaderDescription = mContent.findViewById(R.id.widget_picker_description); + + mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu); + setupWidgetOptionsMenu(); + mRightPane = mContent.findViewById(R.id.right_pane); mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view); mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); @@ -138,6 +163,51 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mFastScroller.setVisibility(GONE); } + @Override + public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) { + if (title != null) { + mHeaderTitle.setText(title); + } + if (description != null) { + mHeaderDescription.setText(description); + mHeaderDescription.setVisibility(VISIBLE); + } + } + + protected void setupWidgetOptionsMenu() { + mWidgetOptionsMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mWidgetOptionsMenuState != null) { + PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v, + Gravity.END); + MenuItem menuItem = popupMenu.getMenu().add( + R.string.widget_picker_show_all_widgets_menu_item_title); + menuItem.setCheckable(true); + menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets); + menuItem.setOnMenuItemClickListener( + item -> onShowAllWidgetsMenuItemClick(item)); + popupMenu.show(); + } + } + }); + } + + private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) { + mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets; + menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets); + + // Refresh widgets + onWidgetsBound(); + if (mIsInSearchMode) { + mSearchBar.reset(); + } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) { + mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); + mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop(); + } + return true; + } + @Override protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) { if (enableCategorizedWidgetSuggestions()) { @@ -216,6 +286,29 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } } + @Override + protected List getWidgetsToDisplay() { + List allWidgets = + mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets(); + List defaultWidgets = + mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets(); + + if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) { + // no menu if there are no default widgets to show + mWidgetOptionsMenuState = null; + mWidgetOptionsMenu.setVisibility(GONE); + } else { + if (mWidgetOptionsMenuState == null) { + mWidgetOptionsMenuState = new WidgetOptionsMenuState(); + } + + mWidgetOptionsMenu.setVisibility(VISIBLE); + return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets; + } + + return allWidgets; + } + @Override public void onWidgetsBound() { super.onWidgetsBound(); @@ -250,14 +343,9 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { false); mSuggestedWidgetsHeader.setExpanded(true); - PackageItemInfo packageItemInfo = new PackageItemInfo( + PackageItemInfo packageItemInfo = new HighresPackageItemInfo( /* packageName= */ SUGGESTIONS_PACKAGE_NAME, - Process.myUserHandle()) { - @Override - public boolean usingLowResIcon() { - return false; - } - }; + Process.myUserHandle()); String suggestionsHeaderTitle = getContext().getString( R.string.suggested_widgets_header_title); String suggestionsRightPaneTitle = getContext().getString( @@ -268,7 +356,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create( packageItemInfo, /*titleSectionName=*/ suggestionsHeaderTitle, - /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(), + /*items=*/ List.of(), // not necessary /*visibleWidgetsCount=*/ 0) .withWidgetListShown(); @@ -281,11 +369,17 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mRightPane.removeAllViews(); mRightPane.addView(mWidgetRecommendationsContainer); mRightPaneScrollView.setScrollY(0); - mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo); final boolean isChangingHeaders = mSelectedHeader == null || !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey); if (isChangingHeaders) { + // If the initial focus view is still focused or widget picker is still opening, it + // is likely a programmatic header click. + if (mSelectedHeader != null && !mOpenCloseAnimation.getAnimationPlayer().isRunning() + && !getAccessibilityInitialFocusView().isAccessibilityFocused()) { + mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle); + focusOnFirstWidgetCell(mWidgetRecommendationsView); + } // If switching from another header, unselect any WidgetCells. This is necessary // because we do not clear/recycle the WidgetCells in the recommendations container // when the header is clicked, only when onRecommendationsBound is called. That @@ -296,7 +390,6 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mSelectedHeader = mSuggestedWidgetsPackageUserKey; }); mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader); - mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); } @Override @@ -312,6 +405,30 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { * RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE; } + @Override + @Px + protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { + int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth); + + if (mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) { + // See onLayout + int leftPaneWidth = (int) (0.33 * pickerAvailableWidth); + @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); + @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); + if (leftPaneWidth < minLeftPaneWidthPx) { + leftPaneWidth = minLeftPaneWidthPx; + } else if (leftPaneWidth > maxLeftPaneWidthPx) { + leftPaneWidth = maxLeftPaneWidthPx; + } + rightPaneWidth = pickerAvailableWidth - leftPaneWidth; + } + + // Since suggestions are shown in right pane, the available width is 2/3 of total width of + // bottom sheet. + return rightPaneWidth - getResources().getDimensionPixelSize( + R.dimen.widget_list_horizontal_margin_two_pane); // right pane end margin. + } + @Override public void onActivePageChanged(int currentActivePage) { super.onActivePageChanged(currentActivePage); @@ -323,21 +440,31 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mActivePage = currentActivePage; - if (mSuggestedWidgetsHeader == null) { - mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); - mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop(); - } else if (currentActivePage == PERSONAL_TAB || currentActivePage == WORK_TAB) { - mSuggestedWidgetsHeader.callOnClick(); - } + // When using talkback, swiping left while on right pane, should navigate to the widgets + // list on left. + mAdapters.get(mActivePage).mWidgetsRecyclerView.setAccessibilityTraversalBefore( + mRightPaneScrollView.getId()); + + // On page change, select the first item in the list to show in the right pane. + mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); + mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop(); } @Override protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { // The first item is always an empty space entry. Look for any more items. boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); - - mRightPane.setVisibility(isWidgetAvailable ? VISIBLE : GONE); - + if (!isWidgetAvailable) { + mRightPane.removeAllViews(); + mRightPane.addView(mNoWidgetsView); + // with no widgets message, no header is selected on left + if (mSuggestedWidgetsPackageUserKey != null + && mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader) + && mSuggestedWidgetsHeader != null) { + mSuggestedWidgetsHeader.setExpanded(false); + } + mSelectedHeader = null; + } super.updateRecyclerViewVisibility(adapterHolder); } @@ -372,20 +499,23 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } - @Override - protected View getContentView() { - return mRightPane; - } - private HeaderChangeListener getHeaderChangeListener() { return new HeaderChangeListener() { @Override public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { final boolean isSameHeader = mSelectedHeader != null && mSelectedHeader.equals(selectedHeader); + // If the initial focus view is still focused or widget picker is still opening, it + // is likely a programmatic header click. + final boolean isUserClick = mSelectedHeader != null + && !mOpenCloseAnimation.getAnimationPlayer().isRunning() + && !getAccessibilityInitialFocusView().isAccessibilityFocused(); mSelectedHeader = selectedHeader; - WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider() - .getSelectedAppWidgets(selectedHeader); + final boolean showDefaultWidgets = mWidgetOptionsMenuState != null + && !mWidgetOptionsMenuState.showAllWidgets; + WidgetsListContentEntry contentEntry = findContentEntryForPackageUser( + mActivityContext.getWidgetPickerDataProvider().get(), + selectedHeader, showDefaultWidgets); if (contentEntry == null || mRightPane == null) { return; @@ -427,11 +557,14 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { }; mRightPane.removeAllViews(); mRightPane.addView(widgetsRowViewHolder.itemView); + if (isUserClick) { + mRightPaneScrollView.setAccessibilityPaneTitle(getContext().getString( + R.string.widget_picker_right_pane_accessibility_title, + contentEntry.mPkgItem.title)); + postDelayed(() -> focusOnFirstWidgetCell(widgetsRowViewHolder.tableContainer), + WIDGET_LIST_ITEM_APPEARANCE_DELAY); + } mRightPaneScrollView.setScrollY(0); - mRightPane.setAccessibilityPaneTitle( - getContext().getString( - R.string.widget_picker_right_pane_accessibility_title, - contentEntry.mPkgItem.title)); } }; } @@ -445,6 +578,18 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } } + /** + * Requests focus on the first widget cell in the given widget section. + */ + private static void focusOnFirstWidgetCell(ViewGroup parent) { + if (parent == null) return; + WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell); + if (cell != null) { + cell.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) { if (parent == null || item == null) return; WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc @@ -504,4 +649,26 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { */ void onHeaderChanged(@NonNull PackageUserKey selectedHeader); } + + /** + * Holds the selection state of the options menu (if presented to the user). + */ + protected static class WidgetOptionsMenuState { + /** + * UI state indicating whether to show default or all widgets. + *

If true, shows all widgets; else shows the default widgets.

+ */ + public boolean showAllWidgets = false; + } + + private static class HighresPackageItemInfo extends PackageItemInfo { + HighresPackageItemInfo(String packageName, UserHandle user) { + super(packageName, user); + } + + @Override + public boolean usingLowResIcon() { + return false; + } + } } diff --git a/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt new file mode 100644 index 0000000000..46d3e7ab1e --- /dev/null +++ b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget.picker.model + +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.widget.model.WidgetsListBaseEntry +import com.android.launcher3.widget.picker.model.data.WidgetPickerData +import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets +import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets +import java.io.PrintWriter + +/** + * Provides [WidgetPickerData] to various views such as widget picker, app-specific widget picker, + * widgets shortcut. + */ +class WidgetPickerDataProvider { + /** All the widgets data provided for the views */ + private var mWidgetPickerData: WidgetPickerData = WidgetPickerData() + + private var changeListener: WidgetPickerDataChangeListener? = null + + /** Sets a listener to be called back when widget data is updated. */ + fun setChangeListener(changeListener: WidgetPickerDataChangeListener?) { + this.changeListener = changeListener + } + + /** Returns the current snapshot of [WidgetPickerData]. */ + fun get(): WidgetPickerData { + return mWidgetPickerData + } + + /** + * Updates the widgets available to the widget picker. + * + * Generally called when the widgets model has new data. + */ + @JvmOverloads + fun setWidgets( + allWidgets: List, + defaultWidgets: List = listOf() + ) { + mWidgetPickerData = + mWidgetPickerData.withWidgets(allWidgets = allWidgets, defaultWidgets = defaultWidgets) + changeListener?.onWidgetsBound() + } + + /** + * Makes the widget recommendations available to the widget picker + * + * Generally called when new widget predictions are available. + */ + fun setWidgetRecommendations(recommendations: List) { + mWidgetPickerData = mWidgetPickerData.withRecommendedWidgets(recommendations) + changeListener?.onRecommendedWidgetsBound() + } + + /** Writes the current state to the provided writer. */ + fun dump(prefix: String, writer: PrintWriter) { + writer.println(prefix + "WidgetPickerDataProvider:") + writer.println("$prefix\twidgetPickerData:$mWidgetPickerData") + } + + interface WidgetPickerDataChangeListener { + /** A callback to get notified when widgets are bound. */ + fun onWidgetsBound() + + /** A callback to get notified when recommended widgets are bound. */ + fun onRecommendedWidgetsBound() + } +} diff --git a/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt new file mode 100644 index 0000000000..3332ef0c16 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget.picker.model.data + +import com.android.launcher3.model.WidgetItem +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.widget.PendingAddWidgetInfo +import com.android.launcher3.widget.model.WidgetsListBaseEntry +import com.android.launcher3.widget.model.WidgetsListContentEntry +import com.android.launcher3.widget.picker.WidgetRecommendationCategory +import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY + +// This file contains WidgetPickerData and utility functions to operate on it. + +/** Widget data for display in the widget picker. */ +data class WidgetPickerData( + val allWidgets: List = listOf(), + val defaultWidgets: List = listOf(), + val recommendations: Map> = mapOf(), +) + +/** Provides utility methods to work with a [WidgetPickerData] object. */ +object WidgetPickerDataUtils { + /** + * Returns a [WidgetPickerData] with the provided widgets. + * + * When [defaultWidgets] is not passed, defaults from previous object are not copied over. + * Defaults (if supported) should be updated when all widgets are updated. + */ + fun WidgetPickerData.withWidgets( + allWidgets: List, + defaultWidgets: List = listOf() + ): WidgetPickerData { + return copy(allWidgets = allWidgets, defaultWidgets = defaultWidgets) + } + + /** Returns a [WidgetPickerData] with the given recommendations set. */ + fun WidgetPickerData.withRecommendedWidgets(recommendations: List): WidgetPickerData { + val allWidgetsMap: Map = + allWidgets + .filterIsInstance() + .flatMap { it.mWidgets } + .filterNotNull() + .distinct() + .associateBy { it } // as ComponentKey + + val categoriesMap = + recommendations + .filterIsInstance() + .filter { allWidgetsMap.containsKey(ComponentKey(it.targetComponent, it.user)) } + .groupBy { it.recommendationCategory ?: DEFAULT_WIDGET_RECOMMENDATION_CATEGORY } + .mapValues { (_, pendingAddWidgetInfos) -> + pendingAddWidgetInfos.map { + allWidgetsMap[ComponentKey(it.targetComponent, it.user)] as WidgetItem + } + } + + return copy(recommendations = categoriesMap) + } + + /** Finds all [WidgetItem]s available for the provided package user. */ + @JvmStatic + fun findAllWidgetsForPackageUser( + widgetPickerData: WidgetPickerData, + packageUserKey: PackageUserKey + ): List { + return findContentEntryForPackageUser(widgetPickerData, packageUserKey)?.mWidgets + ?: emptyList() + } + + /** + * Finds and returns the [WidgetsListContentEntry] for the given package user. + * + * Set [fromDefaultWidgets] to true to limit the content entry to default widgets. + */ + @JvmOverloads + @JvmStatic + fun findContentEntryForPackageUser( + widgetPickerData: WidgetPickerData, + packageUserKey: PackageUserKey, + fromDefaultWidgets: Boolean = false + ): WidgetsListContentEntry? { + val widgetsListBaseEntries = + if (fromDefaultWidgets) { + widgetPickerData.defaultWidgets + } else { + widgetPickerData.allWidgets + } + + return widgetsListBaseEntries.filterIsInstance().firstOrNull { + PackageUserKey.fromPackageItemInfo(it.mPkgItem) == packageUserKey + } + } +} diff --git a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java index 65937b644a..92caf3ec65 100644 --- a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java +++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.R; -import com.android.launcher3.popup.PopupDataProvider; /** * View for a search bar with an edit text with a cancel button. @@ -51,7 +50,8 @@ public class LauncherWidgetsSearchBar extends LinearLayout implements WidgetsSea } @Override - public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) { + public void initialize(WidgetsSearchDataProvider dataProvider, + SearchModeListener searchModeListener) { mController = new WidgetsSearchBarController( new SimpleWidgetsSearchAlgorithm(dataProvider), mEditText, mCancelButton, searchModeListener); diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java index 613066a048..0e88787e12 100644 --- a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java +++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java @@ -21,13 +21,13 @@ import static com.android.launcher3.search.StringMatcherUtility.matches; import android.os.Handler; import com.android.launcher3.model.WidgetItem; -import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.search.SearchAlgorithm; import com.android.launcher3.search.SearchCallback; import com.android.launcher3.search.StringMatcherUtility.StringMatcher; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; +import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider; import java.util.ArrayList; import java.util.List; @@ -39,9 +39,9 @@ import java.util.stream.Collectors; public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm { private final Handler mResultHandler; - private final PopupDataProvider mDataProvider; + private final WidgetsSearchDataProvider mDataProvider; - public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) { + public SimpleWidgetsSearchAlgorithm(WidgetsSearchDataProvider dataProvider) { mResultHandler = new Handler(); mDataProvider = dataProvider; } @@ -63,9 +63,9 @@ public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm getFilteredWidgets( - PopupDataProvider dataProvider, String input) { + WidgetsSearchDataProvider dataProvider, String input) { ArrayList results = new ArrayList<>(); - dataProvider.getAllWidgets().stream() + dataProvider.getWidgets().stream() .filter(entry -> entry instanceof WidgetsListHeaderEntry) .forEach(headerEntry -> { List matchedWidgetItems = filterWidgetItems( diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java index 44a5e80f5e..ab504e7c94 100644 --- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java +++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java @@ -16,7 +16,9 @@ package com.android.launcher3.widget.picker.search; -import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; + +import java.util.List; /** * Interface for a widgets picker search bar. @@ -25,7 +27,7 @@ public interface WidgetsSearchBar { /** * Attaches a controller to the search bar which interacts with {@code searchModeListener}. */ - void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener); + void initialize(WidgetsSearchDataProvider dataProvider, SearchModeListener searchModeListener); /** * Clears search bar. @@ -44,4 +46,15 @@ public interface WidgetsSearchBar { * Sets the vertical location, in pixels, of this search bar relative to its top position. */ void setTranslationY(float translationY); + + + /** + * Provides corpus from which search results must be returned. + */ + interface WidgetsSearchDataProvider { + /** + * Returns the widgets from which the search should return the results. + */ + List getWidgets(); + } } diff --git a/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt index a016676320..1ab8f8bb54 100644 --- a/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt +++ b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt @@ -28,6 +28,7 @@ val HANDHELD_WIDGET_PREVIEW_SIZES: List = WidgetPreviewContainerSize(spanX = 2, spanY = 3), WidgetPreviewContainerSize(spanX = 2, spanY = 2), WidgetPreviewContainerSize(spanX = 4, spanY = 1), + WidgetPreviewContainerSize(spanX = 3, spanY = 1), WidgetPreviewContainerSize(spanX = 2, spanY = 1), WidgetPreviewContainerSize(spanX = 1, spanY = 1), ) diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java index edaf4749b5..df72f07336 100644 --- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java +++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java @@ -68,6 +68,21 @@ public final class WidgetsTableUtils { } }); + /** + * Comparator that enables displaying rows with more number of items at the top, and then + * rest of widgets shown in increasing order of their size (totalW * H). + */ + public static final Comparator> WIDGETS_TABLE_ROW_COUNT_COMPARATOR = + Comparator.comparingInt(row -> { + if (row.size() > 1) { + return -row.size(); + } else { + int rowWidth = row.stream().mapToInt(w -> w.spanX).sum(); + int rowHeight = row.get(0).spanY; + return (rowWidth * rowHeight); + } + }); + /** * Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI * table. This takes liberty to rearrange widgets to make the table visually appealing. diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java similarity index 50% rename from tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java rename to src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java index d3d297003d..63d87e8684 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java +++ b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java @@ -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,20 @@ * limitations under the License. */ -package com.android.launcher3.celllayout.board; +package com.android.launcher3.dagger; -import android.graphics.Point; +import dagger.Component; -public class IconPoint { - public Point coord; - public char mType; - - public IconPoint(Point coord, char type) { - this.coord = coord; - mType = type; - } - - 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; +/** + * Root component for Dagger injection for Launcher AOSP. + */ +@LauncherAppSingleton +@Component +public interface LauncherAppComponent extends LauncherBaseAppComponent { + /** Builder for aosp LauncherAppComponent. */ + @Component.Builder + interface Builder extends LauncherBaseAppComponent.Builder { + LauncherAppComponent build(); } } + diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java index a940774ff0..eaa9ef0b20 100644 --- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java +++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java @@ -49,6 +49,8 @@ public interface LauncherOverlayManager { default void onActivityDestroyed() { } + default void onDisallowSwipeToMinusOnePage() {} + /** * @deprecated use LauncherOverlayTouchProxy directly */ diff --git a/tests/Android.bp b/tests/Android.bp index d6a4fdcf25..9f62d02b64 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -71,6 +71,9 @@ filegroup { // Library with all the dependencies for building quickstep android_library { name: "Launcher3TestLib", + defaults: [ + "launcher_compose_tests_defaults", + ], srcs: [], asset_dirs: ["assets"], resource_dirs: ["res"], @@ -112,6 +115,9 @@ android_library { android_test { name: "Launcher3Tests", + defaults: [ + "launcher_compose_tests_defaults", + ], srcs: [ ":launcher-tests-src", ":launcher-non-quickstep-tests-src", @@ -154,7 +160,7 @@ android_library { } filegroup { - name: "launcher-testing-helpers", + name: "launcher-testing-helpers-robo", srcs: [ "src/**/*.java", "src/**/*.kt", @@ -168,11 +174,20 @@ filegroup { // Test classes "src/**/*Test.java", "src/**/*Test.kt", + "src/**/RoboApiWrapper.kt", "multivalentTests/src/**/*Test.java", "multivalentTests/src/**/*Test.kt", ], } +filegroup { + name: "launcher-testing-helpers", + srcs: [ + ":launcher-testing-helpers-robo", + "src/**/RoboApiWrapper.kt", + ], +} + android_robolectric_test { enabled: true, name: "Launcher3RoboTests", @@ -180,7 +195,7 @@ android_robolectric_test { ":launcher3-robo-src", // Test util classes - ":launcher-testing-helpers", + ":launcher-testing-helpers-robo", ":launcher-testing-shared", ], exclude_srcs: [ diff --git a/tests/assets/ReorderWidgets/full_reorder_case b/tests/assets/ReorderWidgets/full_reorder_case index 850e4fdd0e..2890b79abf 100644 --- a/tests/assets/ReorderWidgets/full_reorder_case +++ b/tests/assets/ReorderWidgets/full_reorder_case @@ -17,12 +17,12 @@ # Test 4x4 board: 4x4 xxxx -bbmm +bbaa +iimm iimm -iiaa arguments: 0 2 board: 4x4 xxxx -bbii +bbaa mmii -mmaa \ No newline at end of file +mmii \ No newline at end of file diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case index 8e845a2462..1eacfaec0f 100644 --- a/tests/assets/ReorderWidgets/push_reorder_case +++ b/tests/assets/ReorderWidgets/push_reorder_case @@ -17,28 +17,28 @@ #Test 5x5 board: 5x5 xxxxx -bbbm- +bbb-- --ccc --ddd ------ -arguments: 2 1 +----m +arguments: 2 2 board: 5x5 xxxxx ---m-- bbb-- +--m-- --ccc --ddd #6x5 Test board: 6x5 xxxxxx -bbbbm- ---aaa- ---ddd- ------- -arguments: 2 1 -board: 6x5 -xxxxxx ---m--- bbbb-- --aaa- +--ddd- +-----m +arguments: 2 2 +board: 6x5 +xxxxxx +bbbb-- +--m--- +--aaa- --ddd- \ No newline at end of file diff --git a/tests/assets/ReorderWidgets/simple_reorder_case b/tests/assets/ReorderWidgets/simple_reorder_case index 2c50ce4d68..991ccb55ef 100644 --- a/tests/assets/ReorderWidgets/simple_reorder_case +++ b/tests/assets/ReorderWidgets/simple_reorder_case @@ -21,7 +21,7 @@ xxxxx --mm- ----- ----- -arguments: 0 4 +arguments: 0 3 board: 5x5 xxxxx ----- diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java index 59d0de69ee..d7dd40bb7f 100644 --- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -148,16 +148,13 @@ public final class TestProtocol { public static final String REQUEST_HOTSEAT_CELL_CENTER = "hotseat-cell-center"; - public static final String REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET = - "get-focused-task-height-for-tablet"; - public static final String REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET = - "get-grid-task-size-rect-for-tablet"; + public static final String REQUEST_GET_OVERVIEW_TASK_SIZE = "get-overivew-task-size"; + public static final String REQUEST_GET_OVERVIEW_GRID_TASK_SIZE = "get-overivew-grid-task-size"; public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing"; public static final String REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX = "get-overview-current-page-index"; public static final String REQUEST_GET_SPLIT_SELECTION_ACTIVE = "get-split-selection-active"; public static final String REQUEST_ENABLE_ROTATION = "enable_rotation"; - public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion"; public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared"; public static boolean sDebugTracing = false; @@ -173,7 +170,6 @@ public final class TestProtocol { public static final String UIOBJECT_STALE_ELEMENT = "b/319501259"; public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466"; public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890"; - public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341"; public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview"; public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs"; diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt index 0538870132..954dc8fb59 100644 --- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt @@ -47,14 +47,13 @@ import org.mockito.kotlin.whenever abstract class FakeInvariantDeviceProfileTest { protected lateinit var context: Context - protected var inv: InvariantDeviceProfile? = null - protected val info: Info = mock() - protected var windowBounds: WindowBounds? = null - protected var isMultiWindowMode: Boolean = false - protected var transposeLayoutWithOrientation: Boolean = false - protected var useTwoPanels: Boolean = false - protected var isGestureMode: Boolean = true - protected var isTransientTaskbar: Boolean = true + protected lateinit var inv: InvariantDeviceProfile + protected val info = mock() + protected lateinit var windowBounds: WindowBounds + private var transposeLayoutWithOrientation = false + private var useTwoPanels = false + private var isGestureMode = true + private var isTransientTaskbar = true @Rule @JvmField val limitDevicesRule = LimitDevicesRule() @@ -73,7 +72,7 @@ abstract class FakeInvariantDeviceProfileTest { info, windowBounds, SparseArray(), - isMultiWindowMode, + /*isMultiWindowMode=*/ false, transposeLayoutWithOrientation, useTwoPanels, isGestureMode, diff --git a/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt new file mode 100644 index 0000000000..c5f9f86859 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.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 + +import com.android.launcher3.util.MainThreadInitializedObject +import com.android.launcher3.util.MainThreadInitializedObject.SandboxApplication +import com.android.launcher3.util.SafeCloseable + +/** + * Initializes [MainThreadInitializedObject] instances for Robolectric tests. + * + * Unlike instrumentation tests, Robolectric creates a new application instance for each test, which + * could cause the various static objects defined in [MainThreadInitializedObject] to leak. Thus, a + * [SandboxApplication] for Robolectric tests can implement this interface to limit the lifecycle of + * these objects to a single test. + */ +interface RoboObjectInitializer { + + /** Overrides an object with [type] to [value]. */ + fun initializeObject(type: MainThreadInitializedObject, value: T) +} diff --git a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt index 60a4197c99..d0aa7a8dd9 100644 --- a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt @@ -17,12 +17,19 @@ package com.android.launcher3 import android.content.Context +import android.content.ContextWrapper +import android.graphics.Rect +import android.graphics.RectF import android.view.View import android.view.ViewGroup import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.util.ActivityContextWrapper -import org.junit.Assert.* +import kotlin.random.Random +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -30,6 +37,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class UtilitiesTest { + companion object { + const val SEED = 827 + } + private lateinit var mContext: Context @Before @@ -94,4 +105,284 @@ class UtilitiesTest { assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop } + + @Test + fun testNumberBounding() { + assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f)) + assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f)) + assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f)) + + assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L)) + assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L)) + assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L)) + + assertEquals(345, Utilities.boundToRange(345, 2, 500)) + assertEquals(400, Utilities.boundToRange(345, 400, 500)) + assertEquals(300, Utilities.boundToRange(345, 2, 300)) + + val random = Random(SEED) + for (i in 1..300) { + val value = random.nextFloat() + val lowerBound = random.nextFloat() + val higherBound = lowerBound + random.nextFloat() + + assertEquals( + "Utilities.boundToRange doesn't match Kotlin coerceIn", + value.coerceIn(lowerBound, higherBound), + Utilities.boundToRange(value, lowerBound, higherBound) + ) + assertEquals( + "Utilities.boundToRange doesn't match Kotlin coerceIn", + value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()), + Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt()) + ) + assertEquals( + "Utilities.boundToRange doesn't match Kotlin coerceIn", + value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()), + Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong()) + ) + assertEquals( + "If the lower bound is higher than lower bound, it should return the lower bound", + higherBound, + Utilities.boundToRange(value, higherBound, lowerBound) + ) + } + } + + @Test + fun testTranslateOverlappingView() { + testConcentricOverlap() + leftDownCornerOverlap() + noOverlap() + } + + /* + Test Case: Rectangle Contained Within Another Rectangle + + +-------------+ <-- exclusionBounds + | | + | +-----+ | + | | | | <-- targetViewBounds + | | | | + | +-----+ | + | | + +-------------+ + */ + private fun testConcentricOverlap() { + val targetView = View(ContextWrapper(getApplicationContext())) + val targetViewBounds = Rect(40, 40, 60, 60) + val inclusionBounds = Rect(0, 0, 100, 100) + val exclusionBounds = Rect(30, 30, 70, 70) + + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_RIGHT + ) + assertEquals(30f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_LEFT + ) + assertEquals(-30f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_DOWN + ) + assertEquals(30f, targetView.translationY) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_UP + ) + assertEquals(-30f, targetView.translationY) + } + + /* + Test Case: Non-Overlapping Rectangles + + +-----------------+ <-- targetViewBounds + | | + | | + +-----------------+ + + +-----------+ <-- exclusionBounds + | | + | | + +-----------+ + */ + private fun noOverlap() { + val targetView = View(ContextWrapper(getApplicationContext())) + val targetViewBounds = Rect(10, 10, 20, 20) + + val inclusionBounds = Rect(0, 0, 100, 100) + val exclusionBounds = Rect(30, 30, 40, 40) + + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_RIGHT + ) + assertEquals(0f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_LEFT + ) + assertEquals(0f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_DOWN + ) + assertEquals(0f, targetView.translationY) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_UP + ) + assertEquals(0f, targetView.translationY) + } + + /* + Test Case: Rectangles Overlapping at Corners + + +------------+ <-- exclusionBounds + | | + +-------+ | + | | | | <-- targetViewBounds + | +------------+ + | | + +-------+ + */ + private fun leftDownCornerOverlap() { + val targetView = View(ContextWrapper(getApplicationContext())) + val targetViewBounds = Rect(20, 20, 30, 30) + + val inclusionBounds = Rect(0, 0, 100, 100) + val exclusionBounds = Rect(25, 25, 35, 35) + + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_RIGHT + ) + assertEquals(15f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_LEFT + ) + assertEquals(-5f, targetView.translationX) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_DOWN + ) + assertEquals(15f, targetView.translationY) + Utilities.translateOverlappingView( + targetView, + targetViewBounds, + inclusionBounds, + exclusionBounds, + Utilities.TRANSLATE_UP + ) + assertEquals(-5f, targetView.translationY) + } + + @Test + fun trim() { + val expectedString = "Hello World" + assertEquals(expectedString, Utilities.trim("Hello World ")) + // Basic trimming + assertEquals(expectedString, Utilities.trim(" Hello World ")) + assertEquals(expectedString, Utilities.trim(" Hello World")) + + // Non-breaking whitespace + assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0")) + + // Whitespace combinations + assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r")) + assertEquals(expectedString, Utilities.trim("\nHello World ")) + + // Null input + assertEquals("", Utilities.trim(null)) + + // Empty String + assertEquals("", Utilities.trim("")) + } + + @Test + fun getProgress() { + // Basic test + assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f) + + // Negative values + assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f) + + // Outside of range + assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f) + } + + @Test + fun scaleRectFAboutPivot() { + // Enlarge + var rectF = RectF(10f, 20f, 50f, 80f) + Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f) + assertEquals(RectF(0f, 5f, 60f, 95f), rectF) + + // Shrink + rectF = RectF(10f, 20f, 50f, 80f) + Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f) + assertEquals(RectF(20f, 35f, 40f, 65f), rectF) + + // No scale + rectF = RectF(10f, 20f, 50f, 80f) + Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f) + assertEquals(RectF(10f, 20f, 50f, 80f), rectF) + } + + @Test + fun rotateBounds() { + var rect = Rect(20, 70, 60, 80) + Utilities.rotateBounds(rect, 100, 100, 0) + assertEquals(Rect(20, 70, 60, 80), rect) + + rect = Rect(20, 70, 60, 80) + Utilities.rotateBounds(rect, 100, 100, 1) + assertEquals(Rect(70, 40, 80, 80), rect) + + // case removed for b/28435189 + // rect = Rect(20, 70, 60, 80) + // Utilities.rotateBounds(rect, 100, 100, 2) + // assertEquals(Rect(40, 20, 80, 30), rect) + + rect = Rect(20, 70, 60, 80) + Utilities.rotateBounds(rect, 100, 100, 3) + assertEquals(Rect(20, 20, 30, 60), rect) + } } diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt new file mode 100644 index 0000000000..e03ee46811 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.accessibility + +import android.content.Context +import android.view.ViewGroup +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.CellLayout +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.util.ActivityContextWrapper +import java.util.function.Function +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AccessibleDragListenerAdapterTest { + + private lateinit var mockViewGroup: ViewGroup + private lateinit var mockChildOne: CellLayout + private lateinit var mockChildTwo: CellLayout + private lateinit var mDelegateFactory: Function + private lateinit var adapter: AccessibleDragListenerAdapter + private lateinit var mContext: Context + + @Before + fun setup() { + mContext = ActivityContextWrapper(getApplicationContext()) + mockViewGroup = mock(ViewGroup::class.java) + mockChildOne = mock(CellLayout::class.java) + mockChildTwo = mock(CellLayout::class.java) + `when`(mockViewGroup.context).thenReturn(mContext) + `when`(mockViewGroup.childCount).thenReturn(2) + `when`(mockViewGroup.getChildAt(0)).thenReturn(mockChildOne) + `when`(mockViewGroup.getChildAt(1)).thenReturn(mockChildTwo) + // Mock Delegate factory + mDelegateFactory = + mock(Function::class.java) as Function + `when`(mDelegateFactory.apply(any())) + .thenReturn(mock(DragAndDropAccessibilityDelegate::class.java)) + adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory) + } + + @Test + fun `onDragStart enables accessible drag for all view children`() { + // Create mock view children + val mockDragObject = mock(DragObject::class.java) + val mockDragOptions = mock(DragOptions::class.java) + + // Action + adapter.onDragStart(mockDragObject, mockDragOptions) + + // Assertion + verify(mockChildOne).setDragAndDropAccessibilityDelegate(any()) + verify(mockChildTwo).setDragAndDropAccessibilityDelegate(any()) + } + + @Test + fun `onDragEnd removes the accessibility delegate`() { + // Action + adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory) + adapter.onDragEnd() + + // Assertion + verify(mockChildOne).setDragAndDropAccessibilityDelegate(null) + verify(mockChildTwo).setDragAndDropAccessibilityDelegate(null) + } + + @Test + fun `onChildViewAdded sets enabled as true for childview`() { + // Action + adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory) + adapter.onChildViewAdded(mockViewGroup, mockChildOne) + + // Assertion + verify(mockChildOne).setDragAndDropAccessibilityDelegate(any()) + } + + @Test + fun `onChildViewRemoved sets enabled as false for childview`() { + // Action + adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory) + adapter.onChildViewRemoved(mockViewGroup, mockChildOne) + + // Assertion + verify(mockChildOne).setDragAndDropAccessibilityDelegate(null) + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt new file mode 100644 index 0000000000..1cbe1df3a5 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.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.launcher3.accessibility // Use the original package + +// Imports +import android.content.Context +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.CellLayout +import com.android.launcher3.folder.FolderPagedView +import com.android.launcher3.util.ActivityContextWrapper +import kotlin.math.min +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FolderAccessibilityHelperTest { + + // Context + private lateinit var mContext: Context + // Mocks + @Mock private lateinit var mockParent: FolderPagedView + @Mock private lateinit var mockLayout: CellLayout + + private var countX = 4 + private var countY = 3 + private var index = 1 + + // System under test + private lateinit var folderAccessibilityHelper: FolderAccessibilityHelper + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mContext = ActivityContextWrapper(getApplicationContext()) + `when`(mockLayout.parent).thenReturn(mockParent) + `when`(mockLayout.context).thenReturn(mContext) + + // mStartPosition isn't recalculated after the constructor + // If you want to create new tests with different starting params, + // rebuild the folderAccessibilityHelper object + val countX = 4 + val countY = 3 + val index = 1 + `when`(mockParent.indexOfChild(mockLayout)).thenReturn(index) + `when`(mockLayout.countX).thenReturn(countX) + `when`(mockLayout.countY).thenReturn(countY) + + folderAccessibilityHelper = FolderAccessibilityHelper(mockLayout) + } + + // Test for intersectsValidDropTarget() + @Test + fun testIntersectsValidDropTarget() { + // Setup + val id = 5 + val allocatedContentSize = 20 + // Make layout function public @VisibleForTesting + `when`(mockParent.allocatedContentSize).thenReturn(allocatedContentSize) + + // Execute + val result = folderAccessibilityHelper.intersectsValidDropTarget(id) + + // Verify + val expectedResult = min(id, allocatedContentSize - (index * countX * countY) - 1) + assertEquals(expectedResult, result) + } + + // Test for getLocationDescriptionForIconDrop() + @Test + fun testGetLocationDescriptionForIconDrop() { + // Setup + val id = 5 + + // Execute + val result = folderAccessibilityHelper.getLocationDescriptionForIconDrop(id) + + // Verify + val expectedResult = "Move to position ${id + (index * countX * countY) + 1}" + assertEquals(expectedResult, result) + } + + // Test for getConfirmationForIconDrop() + @Test + fun testGetConfirmationForIconDrop() { + // Execute + val result = + folderAccessibilityHelper.getConfirmationForIconDrop(0) // Id doesn't matter here + + // Verify + assertEquals("Item moved", result) + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java index d2238ffda4..d93811929b 100644 --- a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java @@ -65,6 +65,7 @@ public class AlphabeticalAppsListTest { private static final int PRIVATE_SPACE_HEADER_ITEM_COUNT = 1; private static final int MAIN_USER_APP_COUNT = 2; private static final int PRIVATE_USER_APP_COUNT = 2; + private static final int VIEW_AT_END_OF_APP_LIST = 1; private static final int NUM_APP_COLS = 4; private static final int NUM_APP_ROWS = 3; private static final int PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT = 1; @@ -107,7 +108,8 @@ public class AlphabeticalAppsListTest { && info.user.equals(MAIN_HANDLE)); assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT - + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size()); + + PRIVATE_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST, + mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList.getAdapterItems().stream().filter(item -> item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size()); @@ -136,7 +138,7 @@ public class AlphabeticalAppsListTest { && info.user.equals(MAIN_HANDLE)); assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT - + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT + + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT + VIEW_AT_END_OF_APP_LIST + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList.getAdapterItems().stream().filter(item -> @@ -166,7 +168,8 @@ public class AlphabeticalAppsListTest { mAlphabeticalAppsList.updateItemFilter(info -> info != null && info.user.equals(MAIN_HANDLE)); - assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT, + assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT + + VIEW_AT_END_OF_APP_LIST, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList .getAdapterItems().stream().filter(item -> @@ -187,8 +190,8 @@ public class AlphabeticalAppsListTest { mAlphabeticalAppsList.updateItemFilter(info -> info != null && info.user.equals(MAIN_HANDLE)); - assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT, - mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT + + VIEW_AT_END_OF_APP_LIST, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList .getAdapterItems().stream().filter(item -> item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size()); @@ -206,7 +209,8 @@ public class AlphabeticalAppsListTest { mAlphabeticalAppsList.updateItemFilter(info -> info != null && info.user.equals(MAIN_HANDLE)); - assertEquals(MAIN_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST, + mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item -> item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size()); assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item -> @@ -222,7 +226,8 @@ public class AlphabeticalAppsListTest { mAlphabeticalAppsList.updateItemFilter(info -> info != null && info.user.equals(MAIN_HANDLE)); - assertEquals(2, mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST, + mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item -> item.itemInfo != null && item.itemInfo.itemType == VIEW_TYPE_PRIVATE_SPACE_HEADER) diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt new file mode 100644 index 0000000000..d2103aeeac --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt @@ -0,0 +1,92 @@ +/* + * 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.allapps + +import android.content.Context +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.Flags +import com.android.launcher3.util.ActivityContextWrapper +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(AndroidJUnit4::class) +class FloatingHeaderViewTest { + + @get:Rule val mSetFlagsRule = SetFlagsRule() + + private lateinit var context: Context + private lateinit var vut: FloatingHeaderView + + @Before + fun setUp() { + context = ActivityContextWrapper(getApplicationContext()) + // TODO(b/352161553): Inflate FloatingHeaderView or R.layout.all_apps_content with proper + // FloatingHeaderView#setup + vut = FloatingHeaderView(context) + vut.onFinishInflate() + } + + @Test + @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR, Flags.FLAG_MULTILINE_SEARCH_BAR) + fun onHeightUpdated_whenNotMultiline_thenZeroHeight() { + vut.setFloatingRowsCollapsed(true) + val beforeHeight = vut.maxTranslation + vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET) + + vut.onHeightUpdated() + + assertThat(vut.maxTranslation).isEqualTo(beforeHeight) + } + + @Test + @EnableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR) + @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR) + fun onHeightUpdated_whenMultiline_thenHeightIsOffset() { + vut.setFloatingRowsCollapsed(true) + vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET) + + vut.onHeightUpdated() + + assertThat(vut.maxTranslation).isEqualTo(HEADER_HEIGHT_OFFSET) + } + + @Test + @DisableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR) + @EnableFlags(Flags.FLAG_FLOATING_SEARCH_BAR) + fun onHeightUpdated_whenFloatingRowsShownAndNotMultiline_thenAddsOnlyFloatingRow() { + // Collapse floating rows and expand to trigger header height calculation + vut.setFloatingRowsCollapsed(true) + vut.setFloatingRowsCollapsed(false) + val defaultHeight = vut.maxTranslation + vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET) + + vut.onHeightUpdated() + + assertThat(vut.maxTranslation).isEqualTo(defaultHeight) + } + + companion object { + private const val HEADER_HEIGHT_OFFSET = 50 + } +} diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java similarity index 97% rename from tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java rename to tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java index 9537e1c63f..1eb4173593 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java @@ -24,7 +24,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.util.ActivityContextWrapper; diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt new file mode 100644 index 0000000000..5bc57b0474 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.celllayout + +import org.junit.Rule +import org.junit.Test + +// @RunWith(AndroidJUnit4::class) b/353965234 +class CellLayoutMethodsTest { + + @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule() + + //@Test + fun pointToCellExact() { + val width = 1000 + val height = 1000 + val columns = 30 + val rows = 30 + val cl = cellLayoutBuilder.createCellLayout(columns, rows, false, width, height) + + val res = intArrayOf(0, 0) + for (col in 0.. (p as Point).x } .thenComparing { p: Any -> (p as Point).y } @@ -120,9 +120,7 @@ class HotseatReorderUnitTest { ) } board.widgets - .sortedWith( - Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY) - ) + .sortedWith(Comparator.comparing(WidgetRect::cellX).thenComparing(WidgetRect::cellY)) .forEach { widget -> addViewInCellLayout( cl, diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java index 8a9711d2be..a62258cb0e 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java @@ -17,6 +17,9 @@ package com.android.launcher3.celllayout; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; +import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -37,6 +40,8 @@ import com.android.launcher3.celllayout.board.WidgetRect; import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator; import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator; import com.android.launcher3.util.ActivityContextWrapper; +import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.launcher3.util.rule.TestStabilityRule.Stability; import com.android.launcher3.views.DoubleShadowBubbleTextView; import org.junit.Rule; @@ -67,6 +72,9 @@ public class ReorderAlgorithmUnitTest { private static final int TOTAL_OF_CASES_GENERATED = 300; private Context mApplicationContext; + @Rule + public TestStabilityRule mTestStabilityRule = new TestStabilityRule(); + @Rule public UnitTestCellLayoutBuilderRule mCellLayoutBuilder = new UnitTestCellLayoutBuilderRule(); @@ -74,6 +82,7 @@ public class ReorderAlgorithmUnitTest { * This test reads existing test cases and makes sure the CellLayout produces the same * output for each of them for a given input. */ + @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) @Test public void testAllCases() throws IOException { List testCases = getTestCases( @@ -116,6 +125,7 @@ public class ReorderAlgorithmUnitTest { /** * Same as above but testing the Multipage CellLayout. */ + @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) @Test public void generateValidTests_Multi() { Random generator = new Random(SEED); @@ -144,8 +154,8 @@ public class ReorderAlgorithmUnitTest { public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX, int spanY, int minSpanX, int minSpanY, boolean isMulti) { - CellLayout cl = mCellLayoutBuilder.createCellLayout(board.getWidth(), board.getHeight(), - isMulti); + CellLayout cl = mCellLayoutBuilder.createCellLayoutDefaultSize(board.getWidth(), + board.getHeight(), isMulti); // The views have to be sorted or the result can vary board.getIcons() diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt index b63966db74..f624be103a 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt @@ -64,11 +64,21 @@ class UnitTestCellLayoutBuilderRule : TestWatcher() { dp.inv.numRows = prevNumRows } - fun createCellLayout(width: Int, height: Int, isMulti: Boolean): CellLayout { + fun createCellLayoutDefaultSize(columns: Int, rows: Int, isMulti: Boolean): CellLayout { + return createCellLayout(columns, rows, isMulti) + } + + fun createCellLayout( + columns: Int, + rows: Int, + isMulti: Boolean, + width: Int = 1000, + height: Int = 1000 + ): CellLayout { val dp = getDeviceProfile() // modify the device profile. - dp.inv.numColumns = if (isMulti) width / 2 else width - dp.inv.numRows = height + dp.inv.numColumns = if (isMulti) columns / 2 else columns + dp.inv.numRows = rows dp.cellLayoutBorderSpacePx = Point(0, 0) val cl = if (isMulti) MultipageCellLayout(getWrappedContext(applicationContext, dp)) @@ -76,8 +86,8 @@ class UnitTestCellLayoutBuilderRule : TestWatcher() { // I put a very large number for width and height so that all the items can fit, it doesn't // need to be exact, just bigger than the sum of cell border cl.measure( - View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) ) return cl } diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt new file mode 100644 index 0000000000..3cbfc5a252 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt @@ -0,0 +1,63 @@ +/* + * 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.celllayout.board + +import android.graphics.Point +import android.graphics.Rect + +/** Represents a widget in a CellLayoutBoard */ +data class WidgetRect( + val type: Char, + val bounds: Rect, +) { + val spanX: Int = bounds.right - bounds.left + 1 + val spanY: Int = bounds.top - bounds.bottom + 1 + val cellY: Int = bounds.bottom + val cellX: Int = bounds.left + + fun shouldIgnore() = type == CellType.IGNORE + + fun contains(x: Int, y: Int) = bounds.contains(x, y) +} + +/** + * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of + * letter in the alphabet, A=2, B=3, C=4 ... etc. + */ +data class FolderPoint(val coord: Point, val type: Char) { + val numberIconsInside: Int = type.code - 'A'.code + 2 +} + +/** Represents an icon in a CellLayoutBoard */ +data class IconPoint(val coord: Point, val type: Char = CellType.ICON) + +object CellType { + // The cells marked by this will be filled by 1x1 widgets and will be ignored when + // validating + const val IGNORE = 'x' + + // The cells marked by this will be filled by app icons + const val ICON = 'i' + + // The cells marked by FOLDER will be filled by folders with 27 app icons inside + const val FOLDER = 'Z' + + // Empty space + const val EMPTY = '-' + + // Widget that will be saved as "main widget" for easier retrieval + const val MAIN_WIDGET = 'm' // Everything else will be consider a widget +} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java index e5ad888a8c..04bfee97be 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java @@ -88,7 +88,7 @@ public class CellLayoutBoard implements Comparable { public WidgetRect getWidgetOfType(char type) { return mWidgetsRects.stream() - .filter(widgetRect -> widgetRect.mType == type).findFirst().orElse(null); + .filter(widgetRect -> widgetRect.getType() == type).findFirst().orElse(null); } public WidgetRect getWidgetAt(int x, int y) { @@ -117,8 +117,8 @@ public class CellLayoutBoard implements Comparable { } private void removeWidgetFromBoard(WidgetRect widget) { - for (int xi = widget.mBounds.left; xi <= widget.mBounds.right; xi++) { - for (int yi = widget.mBounds.bottom; yi <= widget.mBounds.top; yi++) { + for (int xi = widget.getBounds().left; xi <= widget.getBounds().right; xi++) { + for (int yi = widget.getBounds().bottom; yi <= widget.getBounds().top; yi++) { mWidget[xi][yi] = '-'; } } @@ -127,7 +127,7 @@ public class CellLayoutBoard implements Comparable { private void removeOverlappingItems(Rect rect) { // Remove overlapping widgets and remove them from the board mWidgetsRects = mWidgetsRects.stream().filter(widget -> { - if (rect.intersect(widget.mBounds)) { + if (rect.intersect(widget.getBounds())) { removeWidgetFromBoard(widget); return false; } @@ -135,8 +135,8 @@ public class CellLayoutBoard implements Comparable { }).collect(Collectors.toList()); // Remove overlapping icons and remove them from the board mIconPoints = mIconPoints.stream().filter(iconPoint -> { - int x = iconPoint.coord.x; - int y = iconPoint.coord.y; + int x = iconPoint.getCoord().x; + int y = iconPoint.getCoord().y; if (rect.contains(x, y)) { mWidget[x][y] = '-'; return false; @@ -146,8 +146,8 @@ public class CellLayoutBoard implements Comparable { // Remove overlapping folders and remove them from the board mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { - int x = folderPoint.coord.x; - int y = folderPoint.coord.y; + int x = folderPoint.getCoord().x; + int y = folderPoint.getCoord().y; if (rect.contains(x, y)) { mWidget[x][y] = '-'; return false; @@ -159,7 +159,7 @@ public class CellLayoutBoard implements Comparable { private void removeOverlappingItems(Point p) { // Remove overlapping widgets and remove them from the board mWidgetsRects = mWidgetsRects.stream().filter(widget -> { - if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) { + if (IdenticalBoardComparator.Companion.touchesPoint(widget.getBounds(), p)) { removeWidgetFromBoard(widget); return false; } @@ -167,8 +167,8 @@ public class CellLayoutBoard implements Comparable { }).collect(Collectors.toList()); // Remove overlapping icons and remove them from the board mIconPoints = mIconPoints.stream().filter(iconPoint -> { - int x = iconPoint.coord.x; - int y = iconPoint.coord.y; + int x = iconPoint.getCoord().x; + int y = iconPoint.getCoord().y; if (p.x == x && p.y == y) { mWidget[x][y] = '-'; return false; @@ -178,8 +178,8 @@ public class CellLayoutBoard implements Comparable { // Remove overlapping folders and remove them from the board mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { - int x = folderPoint.coord.x; - int y = folderPoint.coord.y; + int x = folderPoint.getCoord().x; + int y = folderPoint.getCoord().y; if (p.x == x && p.y == y) { mWidget[x][y] = '-'; return false; @@ -226,7 +226,7 @@ public class CellLayoutBoard implements Comparable { public void removeItem(char type) { mWidgetsRects.stream() - .filter(widgetRect -> widgetRect.mType == type) + .filter(widgetRect -> widgetRect.getType() == type) .forEach(widgetRect -> removeOverlappingItems( new Point(widgetRect.getCellX(), widgetRect.getCellY()))); } @@ -365,10 +365,10 @@ public class CellLayoutBoard implements Comparable { board.mWidth = lines[0].length(); board.mWidgetsRects = getRects(board.mWidget); board.mWidgetsRects.forEach(widgetRect -> { - if (widgetRect.mType == CellType.MAIN_WIDGET) { + if (widgetRect.getType() == CellType.MAIN_WIDGET) { board.mMain = widgetRect; } - board.mWidgetsMap.put(widgetRect.mType, widgetRect); + board.mWidgetsMap.put(widgetRect.getType(), widgetRect); }); board.mIconPoints = getIconPoints(board.mWidget); board.mFolderPoints = getFolderPoints(board.mWidget); diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java deleted file mode 100644 index 49c146b32a..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java +++ /dev/null @@ -1,32 +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.celllayout.board; - -public class CellType { - // The cells marked by this will be filled by 1x1 widgets and will be ignored when - // validating - public static final char IGNORE = 'x'; - // The cells marked by this will be filled by app icons - public static final char ICON = 'i'; - // The cells marked by FOLDER will be filled by folders with 27 app icons inside - public static final char FOLDER = 'Z'; - // Empty space - public static final char EMPTY = '-'; - // Widget that will be saved as "main widget" for easier retrieval - public static final char MAIN_WIDGET = 'm'; - // Everything else will be consider a widget -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt index a4a420cf59..aacd940460 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt @@ -26,11 +26,11 @@ class IdenticalBoardComparator : Comparator { /** Converts a list of WidgetRect into a map of the count of different widget.bounds */ private fun widgetsToBoundsMap(widgets: List) = - widgets.groupingBy { it.mBounds }.eachCount() + widgets.groupingBy { it.bounds }.eachCount() /** Converts a list of IconPoint into a map of the count of different icon.coord */ private fun iconsToPosCountMap(widgets: List) = - widgets.groupingBy { it.getCoord() }.eachCount() + widgets.groupingBy { it.coord }.eachCount() override fun compare( cellLayoutBoard: CellLayoutBoard, @@ -47,7 +47,7 @@ class IdenticalBoardComparator : Comparator { widgetsToBoundsMap( otherCellLayoutBoard.widgets .filter { !it.shouldIgnore() } - .filter { !overlapsWithIgnored(ignoredRectangles, it.mBounds) } + .filter { !overlapsWithIgnored(ignoredRectangles, it.bounds) } ) if (widgetsMap != otherWidgetMap) { diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java deleted file mode 100644 index 8a427dd81b..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java +++ /dev/null @@ -1,192 +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.celllayout.board; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider; -import static com.android.launcher3.util.WidgetUtils.createWidgetInfo; - -import android.content.ComponentName; -import android.content.Context; -import android.graphics.Rect; -import android.os.Process; -import android.os.UserHandle; -import android.util.Log; - -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.celllayout.FavoriteItemsTransaction; -import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.FolderInfo; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.LauncherAppWidgetInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; - -import java.util.function.Supplier; -import java.util.stream.IntStream; - -public class TestWorkspaceBuilder { - - private static final String TAG = "CellLayoutBoardBuilder"; - private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests."; - private ComponentName mAppComponentName = new ComponentName( - "com.google.android.calculator", "com.android.calculator2.Calculator"); - private UserHandle mMyUser; - - private Context mContext; - - public TestWorkspaceBuilder(Context context) { - mMyUser = Process.myUserHandle(); - mContext = context; - } - - /** - * Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases. - */ - private FavoriteItemsTransaction fillWithWidgets(WidgetRect widgetRect, - FavoriteItemsTransaction transaction, int screenId) { - int initX = widgetRect.getCellX(); - int initY = widgetRect.getCellY(); - for (int x = initX; x < initX + widgetRect.getSpanX(); x++) { - for (int y = initY; y < initY + widgetRect.getSpanY(); y++) { - try { - // this widgets are filling, we don't care if we can't place them - transaction.addItem(createWidgetInCell( - new WidgetRect(CellType.IGNORE, - new Rect(x, y, x, y)), screenId)); - } catch (Exception e) { - Log.d(TAG, "Unable to place filling widget at " + x + "," + y); - } - } - } - return transaction; - } - - private AppInfo getApp() { - return new AppInfo(mAppComponentName, "test icon", mMyUser, - AppInfo.makeLaunchIntent(mAppComponentName)); - } - - /** - * Helper to set the app to use for the test workspace, - * using activity-alias from AndroidManifest-common. - * @param testAppName the android:name field of the test app activity-alias to use - */ - public void setTestAppActivityAlias(String testAppName) { - this.mAppComponentName = new ComponentName( - getInstrumentation().getContext().getPackageName(), - TEST_ACTIVITY_PACKAGE_PREFIX + testAppName - ); - } - - private void addCorrespondingWidgetRect(WidgetRect widgetRect, - FavoriteItemsTransaction transaction, int screenId) { - if (widgetRect.mType == 'x') { - fillWithWidgets(widgetRect, transaction, screenId); - } else { - transaction.addItem(createWidgetInCell(widgetRect, screenId)); - } - } - - /** - * Builds the given board into the transaction - */ - public FavoriteItemsTransaction buildFromBoard(CellLayoutBoard board, - FavoriteItemsTransaction transaction, final int screenId) { - board.getWidgets().forEach( - (widgetRect) -> addCorrespondingWidgetRect(widgetRect, transaction, screenId)); - board.getIcons().forEach((iconPoint) -> - transaction.addItem(() -> createIconInCell(iconPoint, screenId)) - ); - board.getFolders().forEach((folderPoint) -> - transaction.addItem(() -> createFolderInCell(folderPoint, screenId)) - ); - return transaction; - } - - /** - * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should - * be clean otherwise this doesn't overrides the existing icons. - */ - public FavoriteItemsTransaction fillHotseatIcons(FavoriteItemsTransaction transaction) { - IntStream.range(0, InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons) - .forEach(i -> transaction.addItem(() -> getHotseatValues(i))); - return transaction; - } - - private Supplier createWidgetInCell( - WidgetRect widgetRect, int screenId) { - // Create the widget lazily since the appWidgetId can get lost during setup - return () -> { - LauncherAppWidgetProviderInfo info = findWidgetProvider(false); - LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true); - item.cellX = widgetRect.getCellX(); - item.cellY = widgetRect.getCellY(); - item.spanX = widgetRect.getSpanX(); - item.spanY = widgetRect.getSpanY(); - item.screenId = screenId; - return item; - }; - } - - public FolderInfo createFolderInCell(FolderPoint folderPoint, int screenId) { - FolderInfo folderInfo = new FolderInfo(); - folderInfo.screenId = screenId; - folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - folderInfo.cellX = folderPoint.coord.x; - folderInfo.cellY = folderPoint.coord.y; - folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1; - folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null); - - for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) { - folderInfo.add(getDefaultWorkspaceItem(screenId), false); - } - - return folderInfo; - } - - private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.screenId = screenId; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - return item; - } - - private ItemInfo createIconInCell(IconPoint iconPoint, int screenId) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.screenId = screenId; - item.cellX = iconPoint.getCoord().x; - item.cellY = iconPoint.getCoord().y; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - return item; - } - - private ItemInfo getHotseatValues(int x) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.cellX = x; - item.cellY = 0; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.rank = x; - item.screenId = x; - item.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT; - return item; - } -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt new file mode 100644 index 0000000000..8952b8542c --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt @@ -0,0 +1,191 @@ +/* + * 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.celllayout.board + +import android.content.ComponentName +import android.content.Context +import android.graphics.Rect +import android.os.Process +import android.os.UserHandle +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherSettings +import com.android.launcher3.celllayout.FavoriteItemsTransaction +import com.android.launcher3.model.data.AppInfo +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.ui.TestViewHelpers +import com.android.launcher3.util.WidgetUtils +import java.util.function.Supplier + +class TestWorkspaceBuilder(private val mContext: Context) { + + private var appComponentName = + ComponentName("com.google.android.calculator", "com.android.calculator2.Calculator") + private val myUser: UserHandle = Process.myUserHandle() + + /** Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases. */ + private fun fillWithWidgets( + widgetRect: WidgetRect, + transaction: FavoriteItemsTransaction, + screenId: Int + ): FavoriteItemsTransaction { + val initX = widgetRect.cellX + val initY = widgetRect.cellY + for (x in initX until initX + widgetRect.spanX) { + for (y in initY until initY + widgetRect.spanY) { + try { + // this widgets are filling, we don't care if we can't place them + transaction.addItem( + createWidgetInCell(WidgetRect(CellType.IGNORE, Rect(x, y, x, y)), screenId) + ) + } catch (e: Exception) { + Log.d(TAG, "Unable to place filling widget at $x,$y") + } + } + } + return transaction + } + + private fun app() = + AppInfo(appComponentName, "test icon", myUser, AppInfo.makeLaunchIntent(appComponentName)) + + /** + * Helper to set the app to use for the test workspace, using activity-alias from + * AndroidManifest-common. + * + * @param testAppName the android:name field of the test app activity-alias to use + */ + fun setTestAppActivityAlias(testAppName: String) { + appComponentName = + ComponentName( + InstrumentationRegistry.getInstrumentation().context.packageName, + TEST_ACTIVITY_PACKAGE_PREFIX + testAppName + ) + } + + private fun addCorrespondingWidgetRect( + widgetRect: WidgetRect, + transaction: FavoriteItemsTransaction, + screenId: Int + ) { + if (widgetRect.type == 'x') { + fillWithWidgets(widgetRect, transaction, screenId) + } else { + transaction.addItem(createWidgetInCell(widgetRect, screenId)) + } + } + + /** Builds the given board into the transaction */ + fun buildFromBoard( + board: CellLayoutBoard, + transaction: FavoriteItemsTransaction, + screenId: Int + ): FavoriteItemsTransaction { + board.widgets.forEach { addCorrespondingWidgetRect(it, transaction, screenId) } + board.icons.forEach { transaction.addItem { createIconInCell(it, screenId) } } + board.folders.forEach { transaction.addItem { createFolderInCell(it, screenId) } } + return transaction + } + + /** + * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should + * be clean otherwise this doesn't overrides the existing icons. + */ + fun fillHotseatIcons(transaction: FavoriteItemsTransaction): FavoriteItemsTransaction { + for (i in 0.. { + // Create the widget lazily since the appWidgetId can get lost during setup + return Supplier { + WidgetUtils.createWidgetInfo( + TestViewHelpers.findWidgetProvider(false), + ApplicationProvider.getApplicationContext(), + true + ) + .apply { + cellX = widgetRect.cellX + cellY = widgetRect.cellY + spanX = widgetRect.spanX + spanY = widgetRect.spanY + screenId = paramScreenId + } + } + } + + fun createFolderInCell(folderPoint: FolderPoint, paramScreenId: Int): FolderInfo = + FolderInfo().apply { + screenId = paramScreenId + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + cellX = folderPoint.coord.x + cellY = folderPoint.coord.y + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null) + for (i in 0 until folderPoint.numberIconsInside) { + add(getDefaultWorkspaceItem(paramScreenId), false) + } + } + + private fun getDefaultWorkspaceItem(paramScreenId: Int): WorkspaceItemInfo = + WorkspaceItemInfo(app()).apply { + screenId = paramScreenId + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + } + + private fun createIconInCell(iconPoint: IconPoint, paramScreenId: Int) = + WorkspaceItemInfo(app()).apply { + screenId = paramScreenId + cellX = iconPoint.coord.x + cellY = iconPoint.coord.y + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + } + + private fun getHotseatValues(x: Int) = + WorkspaceItemInfo(app()).apply { + cellX = x + cellY = 0 + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + rank = x + screenId = x + container = LauncherSettings.Favorites.CONTAINER_HOTSEAT + } + + companion object { + private const val TAG = "CellLayoutBoardBuilder" + private const val TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests." + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java deleted file mode 100644 index c90ce8504f..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java +++ /dev/null @@ -1,59 +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.celllayout.board; - -import android.graphics.Rect; - -public class WidgetRect { - public char mType; - public Rect mBounds; - - public WidgetRect(char type, Rect bounds) { - this.mType = type; - this.mBounds = bounds; - } - - public int getSpanX() { - return mBounds.right - mBounds.left + 1; - } - - public int getSpanY() { - return mBounds.top - mBounds.bottom + 1; - } - - public int getCellX() { - return mBounds.left; - } - - public int getCellY() { - return mBounds.bottom; - } - - boolean shouldIgnore() { - return this.mType == CellType.IGNORE; - } - - boolean contains(int x, int y) { - return mBounds.contains(x, y); - } - - @Override - public String toString() { - return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY() - + " xs = " + getSpanX() + " ys = " + getSpanY(); - } -} diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt new file mode 100644 index 0000000000..b491f17f5f --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt @@ -0,0 +1,201 @@ +/* + * 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.folder + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.folder.FolderNameInfos.* +import org.junit.Test +import org.junit.runner.RunWith + +data class Label(val index: Int, val label: String, val score: Float) + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FolderNameInfosTest { + + companion object { + val statusList = + listOf( + SUCCESS, + HAS_PRIMARY, + HAS_SUGGESTIONS, + ERROR_NO_PROVIDER, + ERROR_APP_LOOKUP_FAILED, + ERROR_ALL_APP_LOOKUP_FAILED, + ERROR_NO_LABELS_GENERATED, + ERROR_LABEL_LOOKUP_FAILED, + ERROR_ALL_LABEL_LOOKUP_FAILED, + ERROR_NO_PACKAGES, + ) + } + + @Test + fun status() { + assertStatus(statusList) + assertStatus( + listOf( + ERROR_NO_PROVIDER, + ERROR_APP_LOOKUP_FAILED, + ERROR_ALL_APP_LOOKUP_FAILED, + ERROR_NO_LABELS_GENERATED, + ERROR_LABEL_LOOKUP_FAILED, + ERROR_ALL_LABEL_LOOKUP_FAILED, + ERROR_NO_PACKAGES, + ) + ) + assertStatus( + listOf( + SUCCESS, + HAS_PRIMARY, + HAS_SUGGESTIONS, + ) + ) + assertStatus( + listOf( + SUCCESS, + HAS_PRIMARY, + HAS_SUGGESTIONS, + ) + ) + } + + fun assertStatus(statusList: List) { + var infos = FolderNameInfos() + statusList.forEach { infos.setStatus(it) } + assert(infos.status() == statusList.sum()) { + "There is an overlap on the status constants!" + } + } + + @Test + fun hasPrimary() { + assertHasPrimary( + createNameInfos(listOf(Label(0, "label", 1f)), statusList), + hasPrimary = true + ) + assertHasPrimary( + createNameInfos(listOf(Label(1, "label", 1f)), statusList), + hasPrimary = false + ) + assertHasPrimary( + createNameInfos( + listOf(Label(0, "label", 1f)), + listOf( + ERROR_NO_PROVIDER, + ERROR_APP_LOOKUP_FAILED, + ERROR_ALL_APP_LOOKUP_FAILED, + ERROR_NO_LABELS_GENERATED, + ERROR_LABEL_LOOKUP_FAILED, + ERROR_ALL_LABEL_LOOKUP_FAILED, + ERROR_NO_PACKAGES, + ) + ), + hasPrimary = false + ) + } + + private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) = + assert(nameInfos.hasPrimary() == hasPrimary) + + private fun createNameInfos(labels: List