diff --git a/Android.bp b/Android.bp index 417dd47baf..798abd0415 100644 --- a/Android.bp +++ b/Android.bp @@ -13,22 +13,58 @@ // limitations under the License. package { - default_applicable_licenses: ["packages_apps_Launcher3_license"], + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], } min_launcher3_sdk_version = "26" -// Added automatically by a large-scale-change -// See: http://go/android-license-faq -license { - name: "packages_apps_Launcher3_license", - visibility: [":__subpackages__"], - license_kinds: [ - "SPDX-license-identifier-Apache-2.0", - ], - license_text: [ - "LICENSE.txt", - ], +// 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 +filegroup { + name: "launcher-src", + srcs: [ "src/**/*.java", "src/**/*.kt" ], +} + +filegroup { + name: "launcher-quickstep-src", + srcs: [ "quickstep/src/**/*.java", "quickstep/src/**/*.kt" ], +} + +filegroup { + name: "launcher-go-src", + srcs: [ "go/src/**/*.java", "go/src/**/*.kt" ], +} + +filegroup { + name: "launcher-go-quickstep-src", + srcs: [ "go/quickstep/src/**/*.java", "go/quickstep/src/**/*.kt" ], +} + +filegroup { + name: "launcher-src_shortcuts_overrides", + srcs: [ "src_shortcuts_overrides/**/*.java", "src_shortcuts_overrides/**/*.kt" ], +} + +filegroup { + name: "launcher-src_ui_overrides", + srcs: [ "src_ui_overrides/**/*.java", "src_ui_overrides/**/*.kt" ], +} + +filegroup { + name: "launcher-ext_tests", + srcs: [ "ext_tests/**/*.java", "ext_tests/**/*.kt" ], +} + +filegroup { + name: "launcher-quickstep-ext_tests", + srcs: [ "quickstep/ext_tests/**/*.java", "quickstep/ext_tests/**/*.kt" ], +} + +// Proguard files for Launcher3 +filegroup { + name: "launcher-proguard-rules", + srcs: ["proguard.flags"], } android_library { @@ -43,11 +79,11 @@ android_library { "androidx.test.uiautomator_uiautomator", "androidx.preference_preference", "SystemUISharedLib", + "SystemUIAnimationLib", + "launcher-testing-shared", ], srcs: [ "tests/tapl/**/*.java", - "src/com/android/launcher3/ResourceUtils.java", - "src/com/android/launcher3/testing/TestProtocol.java", ], resource_dirs: [ ], manifest: "tests/tapl/AndroidManifest.xml", @@ -117,6 +153,7 @@ android_library { "androidx.cardview_cardview", "com.google.android.material_material", "iconloader_base", + "view_capture" ], manifest: "AndroidManifest-common.xml", sdk_version: "current", @@ -132,7 +169,10 @@ android_library { android_library { name: "Launcher3CommonDepsLib", srcs: ["src_build_config/**/*.java"], - static_libs: ["Launcher3ResLib"], + static_libs: [ + "Launcher3ResLib", + "launcher-testing-shared", + ], sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, manifest: "AndroidManifest-common.xml", @@ -151,14 +191,10 @@ android_app { "Launcher3CommonDepsLib", ], srcs: [ - "src/**/*.java", - "src/**/*.kt", - "src_shortcuts_overrides/**/*.java", - "src_shortcuts_overrides/**/*.kt", - "src_ui_overrides/**/*.java", - "src_ui_overrides/**/*.kt", - "ext_tests/src/**/*.java", - "ext_tests/src/**/*.kt", + ":launcher-src", + ":launcher-src_shortcuts_overrides", + ":launcher-src_ui_overrides", + ":launcher-ext_tests", ], resource_dirs: [ "ext_tests/res", @@ -207,52 +243,21 @@ android_library { "lottie", "SystemUISharedLib", "SystemUI-statsd", + "SystemUIAnimationLib", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", } -// Source code used for test helpers -filegroup { - name: "launcher-src-ext-tests", - srcs: [ - "ext_tests/src/**/*.java", - "ext_tests/src/**/*.kt", - ], -} - -// Common source files used to build launcher -filegroup { - name: "launcher-src-no-build-config", - srcs: [ - "src/**/*.java", - "src/**/*.kt", - "src_shortcuts_overrides/**/*.java", - "src_shortcuts_overrides/**/*.kt", - "quickstep/src/**/*.java", - "quickstep/src/**/*.kt", - ], -} - -// Proguard files for Launcher3 -filegroup { - name: "launcher-proguard-rules", - srcs: ["proguard.pro"], -} - // Library with all the dependencies for building Launcher Go android_library { name: "LauncherGoResLib", srcs: [ - "src/**/*.java", - "src/**/*.kt", - "quickstep/src/**/*.java", - "quickstep/src/**/*.kt", - "go/src/**/*.java", - "go/src/**/*.kt", - "go/quickstep/src/**/*.java", - "go/quickstep/src/**/*.kt", + ":launcher-src", + ":launcher-quickstep-src", + ":launcher-go-src", + ":launcher-go-quickstep-src", ], resource_dirs: [ "go/res", @@ -265,8 +270,10 @@ android_library { static_libs: [ "Launcher3CommonDepsLib", "QuickstepResLib", + "androidx.room_room-runtime", ], - manifest: "quickstep/AndroidManifest-launcher.xml", + plugins: ["androidx.room_room-compiler-plugin"], + manifest: "quickstep/AndroidManifest.xml", additional_manifests: [ "go/AndroidManifest.xml", "AndroidManifest-common.xml", @@ -281,7 +288,9 @@ android_library { android_library { name: "Launcher3QuickStepLib", srcs: [ - ":launcher-src-no-build-config", + ":launcher-src", + ":launcher-quickstep-src", + ":launcher-src_shortcuts_overrides", ], resource_dirs: [], libs: [ @@ -296,6 +305,7 @@ android_library { "SystemUISharedLib", "Launcher3CommonDepsLib", "QuickstepResLib", + "SystemUIAnimationLib", ], manifest: "quickstep/AndroidManifest.xml", platform_apis: true, @@ -304,3 +314,133 @@ android_library { baseline_filename: "lint-baseline-launcher3.xml", }, } + +// Build rule for Launcher3 Go app for Android Go devices. +android_app { + name: "Launcher3Go", + + static_libs: ["Launcher3CommonDepsLib"], + + srcs: [ + ":launcher-src", + ":launcher-go-src", + ":launcher-src_ui_overrides", + ], + + resource_dirs: ["go/res"], + + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + + sdk_version: "current", + min_sdk_version: "current", + target_sdk_version: "current", + privileged: true, + system_ext_specific: true, + overrides: [ + "Home", + "Launcher2", + "Launcher3", + "Launcher3QuickStep", + ], + required: ["privapp_whitelist_com.android.launcher3"], + + additional_manifests: [ + "AndroidManifest.xml", + "AndroidManifest-common.xml", + ], + + manifest: "go/AndroidManifest.xml", + jacoco: { + include_filter: ["com.android.launcher3.*"], + } + +} + +// Build rule for Quickstep app. +android_app { + name: "Launcher3QuickStep", + + static_libs: ["Launcher3QuickStepLib"], + optimize: { + enabled: false, + }, + + platform_apis: true, + min_sdk_version: "current", + target_sdk_version: "current", + + privileged: true, + system_ext_specific: true, + overrides: [ + "Home", + "Launcher2", + "Launcher3", + ], + required: ["privapp_whitelist_com.android.launcher3"], + + resource_dirs: ["quickstep/res"], + + additional_manifests: [ + "quickstep/AndroidManifest-launcher.xml", + "AndroidManifest-common.xml", + ], + + manifest: "quickstep/AndroidManifest.xml", + jacoco: { + include_filter: ["com.android.launcher3.*"], + } + +} + +// Build rule for Launcher3 Go app with quickstep for Android Go devices. +android_app { + name: "Launcher3QuickStepGo", + + static_libs: [ + "SystemUI-statsd", + "SystemUISharedLib", + "LauncherGoResLib", + ], + + platform_apis: true, + min_sdk_version: "current", + target_sdk_version: "current", + + srcs: [ ], + + resource_dirs: [ + "go/quickstep/res", + "go/res", + "quickstep/res", + ], + + optimize: { + proguard_flags_files: ["proguard.flags"], + enabled: true, + }, + + privileged: true, + system_ext_specific: true, + overrides: [ + "Home", + "Launcher2", + "Launcher3", + "Launcher3QuickStep", + ], + required: ["privapp_whitelist_com.android.launcher3"], + + additional_manifests: [ + "go/AndroidManifest.xml", + "go/AndroidManifest-launcher.xml", + "AndroidManifest-common.xml", + ], + + manifest: "quickstep/AndroidManifest.xml", + jacoco: { + include_filter: ["com.android.launcher3.*"], + } + +} + diff --git a/Android.mk b/Android.mk deleted file mode 100644 index ceaaf138f2..0000000000 --- a/Android.mk +++ /dev/null @@ -1,144 +0,0 @@ -# -# Copyright (C) 2013 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -LOCAL_PATH := $(call my-dir) - -# -# Build rule for Launcher3 Go app for Android Go devices. -# -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_MODULE_TAGS := optional -LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib - -LOCAL_SRC_FILES := \ - $(call all-java-files-under, src) \ - $(call all-java-files-under, src_ui_overrides) \ - $(call all-java-files-under, go/src) - -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/go/res - -LOCAL_PROGUARD_FLAG_FILES := proguard.flags - -LOCAL_SDK_VERSION := current -LOCAL_MIN_SDK_VERSION := 26 -LOCAL_PACKAGE_NAME := Launcher3Go -LOCAL_PRIVILEGED_MODULE := true -LOCAL_SYSTEM_EXT_MODULE := true -LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep -LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3 - -LOCAL_FULL_LIBS_MANIFEST_FILES := \ - $(LOCAL_PATH)/AndroidManifest.xml \ - $(LOCAL_PATH)/AndroidManifest-common.xml - -LOCAL_MANIFEST_FILE := go/AndroidManifest.xml -LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE -include $(BUILD_PACKAGE) - -# -# Build rule for Quickstep app. -# -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_MODULE_TAGS := optional - -LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3QuickStepLib -LOCAL_PROGUARD_ENABLED := disabled - -ifneq (,$(wildcard frameworks/base)) - LOCAL_PRIVATE_PLATFORM_APIS := true -else - LOCAL_SDK_VERSION := system_current - LOCAL_MIN_SDK_VERSION := 26 -endif -LOCAL_PACKAGE_NAME := Launcher3QuickStep -LOCAL_PRIVILEGED_MODULE := true -LOCAL_SYSTEM_EXT_MODULE := true -LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 -LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3 - -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res - -LOCAL_FULL_LIBS_MANIFEST_FILES := \ - $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \ - $(LOCAL_PATH)/AndroidManifest-common.xml - -LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml -LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* - -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE -include $(BUILD_PACKAGE) - - -# -# Build rule for Launcher3 Go app with quickstep for Android Go devices. -# -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_MODULE_TAGS := optional - -LOCAL_STATIC_JAVA_LIBRARIES := \ - SystemUI-statsd \ - SystemUISharedLib -ifneq (,$(wildcard frameworks/base)) - LOCAL_PRIVATE_PLATFORM_APIS := true -else - LOCAL_SDK_VERSION := system_current - LOCAL_MIN_SDK_VERSION := 26 -endif -LOCAL_STATIC_ANDROID_LIBRARIES := LauncherGoResLib - -LOCAL_SRC_FILES := \ - $(call all-java-files-under, src) \ - $(call all-java-files-under, quickstep/src) \ - $(call all-java-files-under, go/src) \ - $(call all-java-files-under, go/quickstep/src) - -LOCAL_RESOURCE_DIR := \ - $(LOCAL_PATH)/go/quickstep/res \ - $(LOCAL_PATH)/go/res \ - $(LOCAL_PATH)/quickstep/res - -LOCAL_PROGUARD_FLAG_FILES := proguard.flags -LOCAL_PROGUARD_ENABLED := full - -LOCAL_PACKAGE_NAME := Launcher3QuickStepGo -LOCAL_PRIVILEGED_MODULE := true -LOCAL_SYSTEM_EXT_MODULE := true -LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep -LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3 - -LOCAL_FULL_LIBS_MANIFEST_FILES := \ - $(LOCAL_PATH)/go/AndroidManifest.xml \ - $(LOCAL_PATH)/go/AndroidManifest-launcher.xml \ - $(LOCAL_PATH)/AndroidManifest-common.xml - -LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml -LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE -include $(BUILD_PACKAGE) - - -# ================================================== -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 5abc57118c..7c0b23f708 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -43,9 +43,12 @@ - + + + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml index aa8408bb12..8844207155 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -61,6 +61,7 @@ + diff --git a/OWNERS b/OWNERS index 05fa502b34..2d7a014ce9 100644 --- a/OWNERS +++ b/OWNERS @@ -4,41 +4,37 @@ # People who can approve changes for submission # -alexchau@google.com -andraskloczl@google.com -patmanning@google.com -petrcermak@google.com -pbdr@google.com -kideckel@google.com -stevenckng@google.com -ydixit@google.com -boadway@google.com -alinazaidi@google.com adamcohen@google.com -hyunyoungs@google.com -mrcasey@google.com -sunnygoyal@google.com +alexchau@google.com +andonian@google.com awickham@google.com -twickham@google.com -winsonc@google.com -zakcohen@google.com -santie@google.com -vadimt@google.com -mett@google.com -jonmiranda@google.com -pinyaoting@google.com -sfufa@google.com -gwasserman@google.com -jamesoleary@google.com -joshtrask@google.com -mrenouf@google.com -mkephart@google.com +brdayauon@google.com +brianji@google.com +captaincole@google.com +charlander@google.com +fbaron@google.com +ganjam@google.com hwwang@google.com -tracyzhou@google.com +hyunyoungs@google.com +jagrutdesai@google.com +jeremysim@google.com +jiuyu@google.com +jonmiranda@google.com +kylim@google.com +patmanning@google.com peanutbutter@google.com +pinyaoting@google.com +randypfohl@google.com +saumyaprakash@google.com +sihua@google.com +sunnygoyal@google.com +tracyzhou@google.com +tsuharesu@google.com +twickham@google.com +vadimt@google.com +victortulias@google.com +winsonc@google.com xuqiu@google.com -sreyasr@google.com -thiruram@google.com per-file FeatureFlags.java, globs = set noparent -per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, zakcohen@google.com, mrcasey@google.com, adamcohen@google.com, hyunyoungs@google.com +per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 912395970f..a77791f78b 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -1,2 +1,4 @@ [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT} + +ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check ${PREUPLOAD_FILES} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c79768c145..6b1d07ed3c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id 'com.android.application' version "8.1.1" - id 'com.android.library' version "8.1.1" apply false + id 'com.android.application' version '8.1.2' + id 'com.android.library' version '8.1.2' apply false id 'org.jetbrains.kotlin.android' version "1.9.10" id 'org.jetbrains.kotlin.plugin.parcelize' version "1.9.10" id 'org.jetbrains.kotlin.plugin.serialization' version "1.9.10" @@ -23,7 +23,7 @@ allprojects { compileSdk 34 defaultConfig { minSdk 26 - targetSdk 32 + targetSdk 33 vectorDrawables.useSupportLibrary = true } lint { @@ -77,20 +77,20 @@ final def ciBuild = System.getenv("CI") == "true" final def ciRef = System.getenv("GITHUB_REF") ?: "" final def ciRunNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "" final def isReleaseBuild = ciBuild && ciRef.contains("alpha") -final def devReleaseName = ciBuild ? "Dev (#${ciRunNumber})" : "Dev (${buildCommit})" +final def devReleaseName = ciBuild ? "Dev (#${ciRunNumber})" : "UnOfficial Dev (${buildCommit})" -final def version = "12.1.0" -final def releaseName = "Alpha 4" +final def version = "13" +final def releaseName = "Dev UnOfficial" final def versionDisplayName = "${version} ${isReleaseBuild ? releaseName : devReleaseName}" final def majorVersion = versionDisplayName.split("\\.")[0] final def quickstepMinSdk = "32" -final def quickstepMaxSdk = "32" +final def quickstepMaxSdk = "33" android { namespace "com.android.launcher3" defaultConfig { - versionCode 12_01_00_05 + versionCode 13 versionName "${versionDisplayName}" buildConfigField "String", "VERSION_DISPLAY_NAME", "\"${versionDisplayName}\"" buildConfigField "String", "MAJOR_VERSION", "\"${majorVersion}\"" @@ -165,7 +165,9 @@ android { debug { applicationIdSuffix ".debug" resValue("string", "derived_app_name", "Lawnchair (Debug)") - signingConfig releaseSigning + minifyEnabled true + shrinkResources true + proguardFiles "proguard-android-optimize.txt", "proguard.pro" } release { @@ -300,18 +302,27 @@ android { } } - addFrameworkJar('framework-12l.jar') + addFrameworkJar('android.jar') } dependencies { implementation projects.iconloaderlib implementation projects.searchuilib + // Recents lib dependency - withQuickstepImplementation projects.systemUIShared withQuickstepCompileOnly projects.hiddenApi + withQuickstepImplementation projects.systemUIShared + withQuickstepImplementation projects.systemUIAnim + withQuickstepImplementation projects.systemUIUnFold + withQuickstepImplementation projects.systemUIViewCapture implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd.jar') implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'seeder.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'core.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'core-all.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-core.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'framework-statsd.jar') // Required for AOSP to compile. This is already included in the sysui_shared.jar withoutQuickstepImplementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'plugin_core.jar') @@ -324,7 +335,7 @@ dependencies { implementation 'com.github.ChickenHook:RestrictionBypass:2.2' implementation 'dev.rikka.tools.refine:runtime:4.3.0' - implementation platform("androidx.compose:compose-bom:2023.09.01") + implementation platform("androidx.compose:compose-bom:2023.09.00") implementation "androidx.compose.ui:ui" implementation "androidx.compose.ui:ui-util" debugImplementation "androidx.compose.ui:ui-tooling" @@ -337,7 +348,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.activity:activity-compose:1.7.2" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2" - implementation "androidx.navigation:navigation-compose:2.7.3" + implementation "androidx.navigation:navigation-compose:2.7.2" implementation "androidx.palette:palette-ktx:1.0.0" implementation "androidx.slice:slice-core:1.1.0-alpha02" implementation "com.google.accompanist:accompanist-drawablepainter:$accompanist_version" @@ -345,7 +356,7 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" implementation "com.google.android.material:material:1.9.0" - implementation "io.github.fornewid:material-motion-compose-core:1.1.0" + implementation "io.github.fornewid:material-motion-compose-core:1.0.6" implementation 'dev.kdrag0n:colorkt:1.0.5' implementation 'io.coil-kt:coil-compose:2.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0' @@ -369,8 +380,6 @@ dependencies { implementation 'com.github.samanzamani:PersianDate:1.7.1' implementation 'com.airbnb.android:lottie:6.1.0' - - debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12" } ksp { diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java index 0f61d149d0..4f9c32ade5 100644 --- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java +++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java @@ -16,20 +16,28 @@ package com.android.launcher3.testing; +import static com.android.launcher3.testing.shared.TestProtocol.VIEW_AND_ACTIVITY_LEAKS; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.app.Activity; import android.app.Application; import android.content.Context; import android.os.Binder; import android.os.Bundle; +import android.os.Process; import android.system.Os; +import android.util.Log; import android.view.View; import androidx.annotation.Keep; +import androidx.annotation.Nullable; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; +import com.android.launcher3.ShortcutAndWidgetContainer; +import com.android.launcher3.testing.shared.TestProtocol; import java.util.ArrayList; import java.util.Collection; @@ -124,7 +132,7 @@ public class DebugTestInformationHandler extends TestInformationHandler { } @Override - public Bundle call(String method, String arg) { + public Bundle call(String method, String arg, @Nullable Bundle extras) { final Bundle response = new Bundle(); switch (method) { case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: { @@ -152,11 +160,17 @@ public class DebugTestInformationHandler extends TestInformationHandler { case TestProtocol.REQUEST_VIEW_LEAK: { if (sLeaks == null) sLeaks = new LinkedList(); + Log.d(VIEW_AND_ACTIVITY_LEAKS, "forcefully leaking 2 views"); sLeaks.add(new View(mContext)); sLeaks.add(new View(mContext)); return response; } + case TestProtocol.PRINT_VIEW_LEAK: { + Log.d(VIEW_AND_ACTIVITY_LEAKS, "(pid=" + Process.myPid() + ") sLeaks=" + sLeaks); + return response; + } + case TestProtocol.REQUEST_START_EVENT_LOGGING: { sEvents = new ArrayList<>(); TestLogging.setEventConsumer( @@ -204,6 +218,45 @@ public class DebugTestInformationHandler extends TestInformationHandler { } } + case TestProtocol.REQUEST_USE_TEST_WORKSPACE_LAYOUT: { + useTestWorkspaceLayout( + LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TEST); + return response; + } + + case TestProtocol.REQUEST_USE_TEST2_WORKSPACE_LAYOUT: { + useTestWorkspaceLayout( + LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TEST2); + return response; + } + + case TestProtocol.REQUEST_USE_TAPL_WORKSPACE_LAYOUT: { + useTestWorkspaceLayout( + LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TAPL); + return response; + } + + case TestProtocol.REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT: { + useTestWorkspaceLayout(null); + return response; + } + + case TestProtocol.REQUEST_HOTSEAT_ICON_NAMES: { + return getLauncherUIProperty(Bundle::putStringArrayList, l -> { + ShortcutAndWidgetContainer hotseatIconsContainer = + l.getHotseat().getShortcutsAndWidgets(); + ArrayList hotseatIconNames = new ArrayList<>(); + + for (int i = 0; i < hotseatIconsContainer.getChildCount(); i++) { + // Use unchecked cast to catch changes in hotseat layout + BubbleTextView icon = (BubbleTextView) hotseatIconsContainer.getChildAt(i); + hotseatIconNames.add((String) icon.getText()); + } + + return hotseatIconNames; + }); + } + case TestProtocol.REQUEST_GET_ACTIVITIES_CREATED_COUNT: { response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, sActivitiesCreatedCount); return response; @@ -218,8 +271,27 @@ public class DebugTestInformationHandler extends TestInformationHandler { return response; } + case TestProtocol.REQUEST_MODEL_QUEUE_CLEARED: + return getFromExecutorSync(MODEL_EXECUTOR, Bundle::new); + default: - return super.call(method, arg); + return super.call(method, arg, extras); + } + } + + private void useTestWorkspaceLayout(String layout) { + final long identity = Binder.clearCallingIdentity(); + try { + if (layout != null) { + LauncherSettings.Settings.call(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_SET_USE_TEST_WORKSPACE_LAYOUT_FLAG, + layout); + } else { + LauncherSettings.Settings.call(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CLEAR_USE_TEST_WORKSPACE_LAYOUT_FLAG); + } + } finally { + Binder.restoreCallingIdentity(identity); } } } diff --git a/fill_screens.py b/fill_screens.py old mode 100755 new mode 100644 diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml index ef379069f7..dedeb3376b 100644 --- a/go/AndroidManifest.xml +++ b/go/AndroidManifest.xml @@ -53,6 +53,10 @@ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" android:enabled="false" tools:node="replace" /> + + diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml index 196541f0a9..48650aac20 100644 --- a/go/quickstep/res/layout/overview_actions_container.xml +++ b/go/quickstep/res/layout/overview_actions_container.xml @@ -109,7 +109,7 @@ style="@style/GoOverviewActionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:drawableStart="@drawable/ic_split_screen" + android:drawableStart="@drawable/ic_split_vertical" android:text="@string/action_split" android:theme="@style/ThemeControlHighlightWorkspaceColor" android:visibility="gone" /> diff --git a/go/quickstep/res/values-af/strings.xml b/go/quickstep/res/values-af/strings.xml new file mode 100644 index 0000000000..501d297a8d --- /dev/null +++ b/go/quickstep/res/values-af/strings.xml @@ -0,0 +1,20 @@ + + + "Deel program" + "Luister" + "Vertaal" + "Lens" + "HET DIT" + "KANSELLEER" + "INSTELLINGS" + "Vertaal of luister na teks op skerm" + "Inligting soos teks op jou skerm, webadresse en skermskote kan met Google gedeel word.\n\nGaan na ""Instellings > Programme > Verstekprogramme > Digitale Assistent-program"" om te verander watter inligting jy deel." + "Kies \'n assistent om hierdie kenmerk te gebruik" + "Kies \'n digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" + "Verander jou assistent om hierdie kenmerk te gebruik" + "Verander jou digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" + "Tik hier om na teks op hierdie skerm te luister" + "Tik hier om teks op hierdie skerm te vertaal" + "Hierdie program kan nie gedeel word nie" + diff --git a/go/quickstep/res/values-am/strings.xml b/go/quickstep/res/values-am/strings.xml new file mode 100644 index 0000000000..ffc59d931d --- /dev/null +++ b/go/quickstep/res/values-am/strings.xml @@ -0,0 +1,20 @@ + + + "መተግበሪያን አጋራ" + "ያዳምጡ" + "ተርጉም" + "ሌንስ" + "ገባኝ" + "ይቅር" + "ቅንብሮች" + "በማያ ገጹ ላይ ጽሑፍን ይተረጉሙ ወይም ያዳምጡ" + "እንደ በማያ ገፅዎ ላይ ያለ ጽሁፍ፣ የድር አድራሻዎች እና ቅጽበታዊ ገፅ እይታዎች ያሉ መረጃዎች ለGoogle ሊጋሩ ይችላሉ።\n\nምን መረጃ እንደሚያጋሩ ለመቀየር ወደ ""ቅንብሮች > መተግበሪያዎች > ነባሪ መተግበሪያዎች > ዲጂታል ረዳት መተግበሪያ"" ይሂዱ።" + "ይህንን ባህሪ ለመጠቀም ረዳት ይምረጡ" + "በማያ ገጽዎ ላይ ጽሑፍን ለማዳመጥ ወይም ለመተርጎም በቅንብሮች ውስጥ የዲጂታል ረዳት መተግበሪያን ይምረጡ" + "ይህንን ባህሪ ለመጠቀም ረዳትዎን ይቀይሩ" + "በማያ ገጽዎ ላይ ጽሑፍን ለማዳመጥ ወይም ለመተርጎም በቅንብሮች ውስጥ የዲጂታል ረዳት መተግበሪያዎን ይቀይሩ" + "በዚህ ማያ ገፅ ላይ ጽሁፍ ለማዳመጥ እዚህ መታ ያድርጉ" + "በዚህ ማያ ገፅ ላይ ጽሁፍ ለመተርጎም እዚህ መታ ያድርጉ" + "ይህ መተግበሪያ ሊጋራ አይችልም" + diff --git a/go/quickstep/res/values-ar/strings.xml b/go/quickstep/res/values-ar/strings.xml index 323e7b09d9..7d981849a1 100644 --- a/go/quickstep/res/values-ar/strings.xml +++ b/go/quickstep/res/values-ar/strings.xml @@ -16,4 +16,5 @@ "للاستماع للنص الظاهر على الشاشة أو ترجمته، عليك تغيير تطبيق المساعد الرقمي في الإعدادات." "انقر هنا للاستماع للنص الظاهر على هذه الشاشة." "انقر هنا لترجمة النص الظاهر على هذه الشاشة." + "لا يمكن مشاركة هذا التطبيق." diff --git a/go/quickstep/res/values-as/strings.xml b/go/quickstep/res/values-as/strings.xml new file mode 100644 index 0000000000..8bf83efcd1 --- /dev/null +++ b/go/quickstep/res/values-as/strings.xml @@ -0,0 +1,20 @@ + + + "এপ্‌ শ্বেয়াৰ কৰক" + "শুনক" + "অনুবাদ কৰক" + "লেন্স" + "বুজি পালোঁ" + "বাতিল কৰক" + "ছেটিং" + "স্ক্ৰীনত থকা পাঠ অনুবাদ কৰক অথবা শুনক" + "আপোনাৰ স্ক্ৰীনত থকা পাঠ, ৱেব ঠিকনা আৰু স্ক্ৰীনশ্বটৰ দৰে তথ্য Googleৰ সৈতে হয়তো শ্বেয়াৰ কৰা হ’ব।\n\nআপুনি কোনসমুহ তথ্য শ্বেয়াৰ কৰিব সেয়া সলনি কৰিবলৈ, ""ছেটিং > এপ্‌ > ডিফ’ল্ট এপ্‌ > ডিজিটেল সহায়ক এপ""লৈ যাওক।" + "এই সুবিধাটো ব্যৱহাৰ কৰিবলৈ এটা সহায়ক বাছনি কৰক" + "আপোনাৰ স্ক্ৰীনত থকা পাঠ শুনিবলৈ অথবা সেই পাঠৰ অনুবাদ কৰিবলৈ, ছেটিঙত এটা ডিজিটেল সহায়ক এপ্‌ বাছনি কৰক" + "এই সুবিধাটো ব্যৱহাৰ কৰিবলৈ আপোনাৰ সহায়ক সলনি কৰক" + "আপোনাৰ স্ক্ৰীনত থকা পাঠ শুনিবলৈ অথবা সেই পাঠৰ অনুবাদ কৰিবলৈ, ছেটিঙত আপোনাৰ ডিজিটেল সহায়ক এপ্‌টো সলনি কৰক" + "এই স্ক্ৰীনখনত থকা পাঠ শুনিবলৈ ইয়াত টিপক" + "এই স্ক্ৰীনখনত থকা পাঠৰ অনুবাদ কৰিবলৈ ইয়াত টিপক" + "এই এপ্‌টো শ্বেয়াৰ কৰিব নোৱাৰি" + diff --git a/go/quickstep/res/values-az/strings.xml b/go/quickstep/res/values-az/strings.xml new file mode 100644 index 0000000000..2b03b1609f --- /dev/null +++ b/go/quickstep/res/values-az/strings.xml @@ -0,0 +1,20 @@ + + + "Tətbiqi paylaşın" + "Dinləyin" + "Tərcümə" + "Linza" + "ANLADIM" + "LƏĞV EDİN" + "AYARLAR" + "Ekrandakı mətni tərcümə edin və ya dinləyin" + "Ekrandakı mətn, veb ünvanlar və ekran görüntüləri kimi məlumatlar Google ilə paylaşıla bilər.\n\nHansı məlumatların paylaşılmasını dəyişmək üçün ""Ayarlar > Tətbiqlər > Defolt tətbiqlər > Rəqəmsal assistent tətbiqi"" bölməsinə keçin." + "Bu funksiyadan istifadə etmək üçün assistenti seçin" + "Ekrandakı mətni dinləmək və ya tərcümə etmək üçün Ayarlarda rəqəmsal assistent tətbiqini seçin" + "Bu funksiyadan istifadə etmək üçün assistenti dəyişin" + "Ekrandakı mətni dinləmək və ya tərcümə etmək üçün Ayarlarda rəqəmsal assistent tətbiqini dəyişin" + "Bu ekrandakı mətni dinləmək üçün buraya toxunun" + "Bu ekrandakı mətni tərcümə etmək üçün buraya toxunun" + "Bu tətbiqi paylaşmaq mümkün deyil" + diff --git a/go/quickstep/res/values-b+sr+Latn/strings.xml b/go/quickstep/res/values-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..263f0118b0 --- /dev/null +++ b/go/quickstep/res/values-b+sr+Latn/strings.xml @@ -0,0 +1,20 @@ + + + "Deli aplikaciju" + "Pusti" + "Prevedi" + "Objektiv" + "VAŽI" + "OTKAŽI" + "PODEŠAVANJA" + "Prevodite ili slušajte tekst na ekranu" + "Informacije poput teksta na ekranu, veb-adresa i snimaka ekrana mogu da se dele sa Google-om.\n\nDa biste promenili informacije koje delite, idite u ""Podešavanja > Aplikacije > Podrazumevane aplikacije > Aplikacija digitalnog pomoćnika""." + "Odaberite pomoćnika da biste koristili ovu funkciju" + "Da biste čuli tekst sa ekrana ili ga preveli, odaberite aplikaciju digitalnog pomoćnika u Podešavanjima" + "Promenite pomoćnika da biste koristili ovu funkciju" + "Da biste čuli tekst sa ekrana ili ga preveli, promenite aplikaciju digitalnog pomoćnika u Podešavanjima" + "Dodirnite ovde da biste čuli tekst sa ovog ekrana" + "Dodirnite ovde da biste preveli tekst sa ovog ekrana" + "Ova aplikacija ne može da se deli" + diff --git a/go/quickstep/res/values-be/strings.xml b/go/quickstep/res/values-be/strings.xml new file mode 100644 index 0000000000..83374bb397 --- /dev/null +++ b/go/quickstep/res/values-be/strings.xml @@ -0,0 +1,20 @@ + + + "Абагуліць праграму" + "Праслухаць" + "Перакласці" + "Аб\'ектыў" + "ЗРАЗУМЕЛА" + "СКАСАВАЦЬ" + "НАЛАДЫ" + "Перакласці ці праслухаць тэкст на экране" + "Такая інфармацыя з вашага экрана, як тэксты, вэб-адрасы і здымкі экрана, можа абагульвацца з Google.\n\nКаб змяніць тып інфармацыі для абагульвання, адкрыйце ""Налады > Праграмы > Стандартныя праграмы > Лічбавы памочнік""." + "Для карыстання гэтай функцыяй выберыце памочніка" + "Каб праслухаць або перакласці тэкст на экране, выберыце ў Наладах лічбавага памочніка" + "Для карыстання гэтай функцыяй змяніце памочніка" + "Каб праслухаць ці перакласці тэкст на экране, змяніце ў Наладах лічбавага памочніка" + "Націсніце тут, каб праслухаць тэкст на экране" + "Націсніце тут, каб перакласці тэкст на экране" + "Не ўдалося абагуліць гэту праграму" + diff --git a/go/quickstep/res/values-bg/strings.xml b/go/quickstep/res/values-bg/strings.xml new file mode 100644 index 0000000000..594189893e --- /dev/null +++ b/go/quickstep/res/values-bg/strings.xml @@ -0,0 +1,20 @@ + + + "Споделяне на прилож." + "Слушане" + "Превод" + "Обектив" + "РАЗБРАХ" + "ОТКАЗ" + "НАСТРОЙКИ" + "Превеждане или четене на текста на екрана" + "Определена информация може да бъде споделена с Google, като например текстът на екрана ви, уеб адресите и екранните снимки.\n\nЗа да промените информацията, която споделяте, отворете ""„Настройки“ > „Приложения“ > „Приложения по подразбиране“ > „Приложение за дигитален асистент“""." + "Изберете приложение за асистент, за да използвате тази функция" + "За да слушате или превеждате текст на екрана си, от настройките изберете приложение за дигитален асистент" + "Изберете приложение за асистент, за да използвате тази функция" + "За да слушате или превеждате текст на екрана си, от настройките променете приложението си за дигитален асистент" + "Докоснете тук, за да слушате текста на този екран" + "Докоснете тук, за да преведете текста на този екран" + "Това приложение не може да бъде споделено" + diff --git a/go/quickstep/res/values-bn/strings.xml b/go/quickstep/res/values-bn/strings.xml index 5485d6bbf5..8c27e63376 100644 --- a/go/quickstep/res/values-bn/strings.xml +++ b/go/quickstep/res/values-bn/strings.xml @@ -16,4 +16,5 @@ "আপনার স্ক্রিনে থাকা টেক্সট শুনতে বা অনুবাদ করতে, সেটিংস থেকে ডিজিটাল অ্যাসিস্ট্যান্ট অ্যাপ পরিবর্তন করুন" "স্ক্রিনে থাকা টেক্সট শুনতে এখানে ট্যাপ করুন" "স্ক্রিনে থাকা টেক্সট অনুবাদ করতে এখানে ট্যাপ করুন" + "এই অ্যাপ শেয়ার করা যাবে না" diff --git a/go/quickstep/res/values-bs/strings.xml b/go/quickstep/res/values-bs/strings.xml new file mode 100644 index 0000000000..7db34ae921 --- /dev/null +++ b/go/quickstep/res/values-bs/strings.xml @@ -0,0 +1,20 @@ + + + "Dijeli aplikaciju" + "Poslušajte" + "Prevedi" + "Objektiv" + "RAZUMIJEM" + "OTKAŽI" + "POSTAVKE" + "Prevedite ili slušajte tekst na ekranu" + "Informacije kao što su tekst na ekranu, web adrese i snimci ekrana mogu se dijeliti s Googleom.\n\nDa promijenite koje informacije dijelite, idite u ""Postavke > Aplikacije > Zadane aplikacije > Aplikacija digitalnog asistenta""." + "Odaberite asistenta da koristite ovu funkciju" + "Da slušate ili prevedete tekst na ekranu, odaberite aplikaciju digitalnog asistenta u Postavkama" + "Promijenite asistenta da koristite ovu funkciju" + "Da slušate ili prevedete tekst na ekranu, promijenite aplikaciju digitalnog asistenta u Postavkama" + "Dodirnite ovdje da slušate tekst na ovom ekranu" + "Dodirnite ovdje da prevedete tekst na ovom ekranu" + "Nije moguće dijeliti ovu aplikaciju" + diff --git a/go/quickstep/res/values-ca/strings.xml b/go/quickstep/res/values-ca/strings.xml new file mode 100644 index 0000000000..889f50eec0 --- /dev/null +++ b/go/quickstep/res/values-ca/strings.xml @@ -0,0 +1,20 @@ + + + "Comparteix aplicació" + "Escolta" + "Tradueix" + "Lens" + "ENTESOS" + "CANCEL·LA" + "CONFIGURACIÓ" + "Tradueix o escolta el text en pantalla" + "És possible que determinada informació es comparteixi amb Google, com ara el text en pantalla, les adreces web i les captures de pantalla.\n\nPer canviar quina informació comparteixes, ves a ""Configuració > Aplicacions > Aplicacions predeterminades > Aplicació de l\'assistent digital""." + "Tria un assistent per utilitzar aquesta funció" + "Per escoltar o traduir text en pantalla, tria una aplicació d\'assistent digital a Configuració" + "Canvia l\'assistent per utilitzar aquesta funció" + "Per escoltar o traduir text en pantalla, canvia l\'aplicació d\'assistent digital a Configuració" + "Toca aquí per escoltar text en pantalla" + "Toca aquí per traduir text en pantalla" + "Aquesta aplicació o es pot compartir" + diff --git a/go/quickstep/res/values-cs/strings.xml b/go/quickstep/res/values-cs/strings.xml new file mode 100644 index 0000000000..b569dfcaaa --- /dev/null +++ b/go/quickstep/res/values-cs/strings.xml @@ -0,0 +1,20 @@ + + + "Sdílet aplikaci" + "Poslechnout" + "Přeložit" + "Lens" + "ROZUMÍM" + "ZRUŠIT" + "NASTAVENÍ" + "Překládejte nebo poslouchejte text na obrazovce" + "S Googlem mohou být sdílena data, jako je text na obrazovce, webové adresy a snímky obrazovky.\n\nSdílená data můžete upřesnit v ""Nastavení > Aplikace > Výchozí aplikace > Aplikace digitálního asistenta""." + "Pokud chcete použít tuto funkci, vyberte asistenta" + "Pokud si chcete poslechnout nebo přeložit text na obrazovce, v Nastavení vyberte aplikaci digitálního asistenta" + "Pokud chcete použít tuto funkci, změňte asistenta" + "Pokud si chcete poslechnout nebo přeložit text na obrazovce, v Nastavení změňte aplikaci digitálního asistenta" + "Klepnutím sem si poslechnete text na této obrazovce" + "Klepnutím sem přeložíte text na této obrazovce" + "Tuto aplikaci nelze sdílet" + diff --git a/go/quickstep/res/values-da/strings.xml b/go/quickstep/res/values-da/strings.xml new file mode 100644 index 0000000000..8f2055bf81 --- /dev/null +++ b/go/quickstep/res/values-da/strings.xml @@ -0,0 +1,20 @@ + + + "Del app" + "Lyt" + "Oversæt" + "Lens" + "OK" + "ANNULLER" + "INDSTILLINGER" + "Oversæt eller hør tekst på skærmen" + "Oplysninger såsom tekst på din skærm, webadresser og screenshots deles muligvis med Google.\n\nHvis du vil ændre, hvilke oplysninger du deler, kan du gå til ""Indstillinger > Apps > Standardapps > App for digital assistent""." + "Vælg en assistent for at bruge denne funktion" + "Hvis du vil høre eller oversætte tekst på din skærm, skal du vælge en digital assistent i Indstillinger" + "Skift assistent for at bruge denne funktion" + "Hvis du vil høre eller oversætte tekst på din skærm, skal du ændre din digitale assistent i Indstillinger" + "Tryk her for at høre teksten på denne skærm" + "Tryk her for at oversætte teksten på denne skærm" + "Denne app kan ikke deles" + diff --git a/go/quickstep/res/values-de/strings.xml b/go/quickstep/res/values-de/strings.xml new file mode 100644 index 0000000000..efc11c94be --- /dev/null +++ b/go/quickstep/res/values-de/strings.xml @@ -0,0 +1,20 @@ + + + "App teilen" + "Anhören" + "Übersetzen" + "Lens" + "OK" + "ABBRECHEN" + "EINSTELLUNGEN" + "Text auf dem Bildschirm übersetzen oder anhören" + "Informationen, wie Text auf deinem Bildschirm, Webadressen und Screenshots, werden möglicherweise an Google weitergegeben.\n\nWenn du ändern möchtest, welche Informationen weitergegeben werden, gehe zu ""Einstellungen > Apps > Standard-Apps > App für digitalen Assistenten""." + "Assistenten auswählen, um diese Funktion zu nutzen" + "Wenn du dir auf deinem Display Text anhören oder übersetzen lassen möchtest, wähle in den Einstellungen eine App für einen digitalen Assistenten" + "Assistenten ändern, um diese Funktion zu nutzen" + "Wenn du dir auf deinem Display Text anhören oder übersetzen lassen möchtest, ändere in den Einstellungen deine App für den digitalen Assistenten" + "Hier tippen, um dir Text auf diesem Display anzuhören" + "Hier tippen, um dir Text auf diesem Display übersetzen zu lassen" + "Diese App kann nicht gemeinsam genutzt werden" + diff --git a/go/quickstep/res/values-el/strings.xml b/go/quickstep/res/values-el/strings.xml new file mode 100644 index 0000000000..9a67420f30 --- /dev/null +++ b/go/quickstep/res/values-el/strings.xml @@ -0,0 +1,20 @@ + + + "Κοινή χρήση εφαρμογ." + "Ακρόαση" + "Μετάφραση" + "Lens" + "ΤΟ ΚΑΤΑΛΑΒΑ" + "ΑΚΥΡΩΣΗ" + "ΡΥΘΜΙΣΕΙΣ" + "Μετάφραση ή ακρόαση του κειμένου στην οθόνη" + "Πληροφορίες όπως είναι το κείμενο στην οθόνη, οι διευθύνσεις ιστού και τα στιγμιότυπα οθόνης, ενδέχεται να κοινοποιηθούν στην Google.\n\nΓια να αλλάξετε τις πληροφορίες που κοινοποιείτε, μεταβείτε στις ""Ρυθμίσεις > Εφαρμογές > Προεπιλεγμένες εφαρμογές > Εφαρμογή ψηφιακού βοηθού""." + "Επιλέξτε έναν βοηθό για να χρησιμοποιήσετε αυτήν τη λειτουργία" + "Για να ακούσετε ή να μεταφράσετε κείμενο στην οθόνη σας, επιλέξτε μια εφαρμογή ψηφιακού βοηθού στις Ρυθμίσεις." + "Αλλάξτε τον βοηθό σας για να χρησιμοποιήσετε αυτήν τη λειτουργία" + "Για να ακούσετε ή να μεταφράσετε κείμενο στην οθόνη σας, αλλάξτε την εφαρμογή ψηφιακού βοηθού στις Ρυθμίσεις." + "Πατήστε εδώ για να ακούσετε το κείμενο σε αυτήν την οθόνη" + "Πατήστε εδώ για να μεταφράσετε το κείμενο σε αυτήν την οθόνη" + "Δεν είναι δυνατή η κοινή χρήση της εφαρμογής" + diff --git a/go/quickstep/res/values-en-rAU/strings.xml b/go/quickstep/res/values-en-rAU/strings.xml new file mode 100644 index 0000000000..676ac43284 --- /dev/null +++ b/go/quickstep/res/values-en-rAU/strings.xml @@ -0,0 +1,20 @@ + + + "Share app" + "Listen" + "Translate" + "Lens" + "OK" + "CANCEL" + "SETTINGS" + "Translate or listen to text on screen" + "Information such as text on your screen, web addresses and screenshots may be shared with Google.\n\nTo change what information you share, go to ""Settings > Apps > Default apps > Digital assistant app""." + "Choose an assistant to use this feature" + "To listen to or translate text on your screen, choose a digital assistant app in settings" + "Change your assistant to use this feature" + "To listen to or translate text on your screen, change your digital assistant app in settings" + "Tap here to listen to text on this screen" + "Tap here to translate text on this screen" + "This app can’t be shared" + diff --git a/go/quickstep/res/values-en-rCA/strings.xml b/go/quickstep/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..664bd94219 --- /dev/null +++ b/go/quickstep/res/values-en-rCA/strings.xml @@ -0,0 +1,20 @@ + + + "Share App" + "Listen" + "Translate" + "Lens" + "GOT IT" + "CANCEL" + "SETTINGS" + "Translate or listen to text on screen" + "Information such as text on your screen, web addresses, and screenshots may be shared with Google.\n\nTo change what information you share, go to ""Settings > Apps > Default apps > Digital assistant app""." + "Choose an assistant to use this feature" + "To listen to or translate text on your screen, choose a digital assistant app in Settings" + "Change your assistant to use this feature" + "To listen to or translate text on your screen, change your digital assistant app in Settings" + "Tap here to listen to text on this screen" + "Tap here to translate text on this screen" + "This app can’t be shared" + diff --git a/go/quickstep/res/values-en-rGB/strings.xml b/go/quickstep/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..676ac43284 --- /dev/null +++ b/go/quickstep/res/values-en-rGB/strings.xml @@ -0,0 +1,20 @@ + + + "Share app" + "Listen" + "Translate" + "Lens" + "OK" + "CANCEL" + "SETTINGS" + "Translate or listen to text on screen" + "Information such as text on your screen, web addresses and screenshots may be shared with Google.\n\nTo change what information you share, go to ""Settings > Apps > Default apps > Digital assistant app""." + "Choose an assistant to use this feature" + "To listen to or translate text on your screen, choose a digital assistant app in settings" + "Change your assistant to use this feature" + "To listen to or translate text on your screen, change your digital assistant app in settings" + "Tap here to listen to text on this screen" + "Tap here to translate text on this screen" + "This app can’t be shared" + diff --git a/go/quickstep/res/values-en-rIN/strings.xml b/go/quickstep/res/values-en-rIN/strings.xml new file mode 100644 index 0000000000..676ac43284 --- /dev/null +++ b/go/quickstep/res/values-en-rIN/strings.xml @@ -0,0 +1,20 @@ + + + "Share app" + "Listen" + "Translate" + "Lens" + "OK" + "CANCEL" + "SETTINGS" + "Translate or listen to text on screen" + "Information such as text on your screen, web addresses and screenshots may be shared with Google.\n\nTo change what information you share, go to ""Settings > Apps > Default apps > Digital assistant app""." + "Choose an assistant to use this feature" + "To listen to or translate text on your screen, choose a digital assistant app in settings" + "Change your assistant to use this feature" + "To listen to or translate text on your screen, change your digital assistant app in settings" + "Tap here to listen to text on this screen" + "Tap here to translate text on this screen" + "This app can’t be shared" + diff --git a/go/quickstep/res/values-en-rXC/strings.xml b/go/quickstep/res/values-en-rXC/strings.xml new file mode 100644 index 0000000000..b6a4021af9 --- /dev/null +++ b/go/quickstep/res/values-en-rXC/strings.xml @@ -0,0 +1,20 @@ + + + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‎‏‏‏‎‏‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‏‏‏‏‎‏‎‎‎Share App‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‎‏‎‎‎‎‎‎‏‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‎‏‎‏‏‎‎‎‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‏‎‎Listen‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‏‏‎‎‏‎‎Translate‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‎‏‎‎‎‏‏‎‎‏‏‎‎‏‎‎‎‎Lens‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎‏‎‏‏‎‏‎‎‎‎‏‏‎‏‎‎‎‏‏‎‎‏‏‎‎‏‏‎‏‎‏‎‏‏‏‎‏‎‎‎GOT IT‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‎‎‏‏‎‏‏‎‏‎‏‎‏‎‏‏‎‎‎‏‎‏‎‏‏‏‎‎CANCEL‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‎‏‏‎‎‏‎‏‏‎‏‏‎‎‎‏‏‎‏‎‏‎‎‏‏‎‎‎‎‏‎‎‏‎‎‎‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‎‎‎SETTINGS‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‎Translate or listen to text on screen‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‏‏‎‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‏‎‏‏‏‎‎‎‏‎‎‎‎‏‎‏‎‏‎‏‎‏‎‎‏‎Information such as text on your screen, web addresses, and screenshots may be shared with Google.‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎To change what information you share, go to ‎‏‎‎‏‏‎""‎‏‎‎‏‏‏‎Settings > Apps > Default apps > Digital assistant app‎‏‎‎‏‏‎""‎‏‎‎‏‏‏‎.‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‏‎‎‎‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎‎‎Choose an assistant to use this feature‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‎‎‏‏‎‎‏‎‏‎‎‎‎‎‎‏‎‎‏‏‎‏‎‎‏‏‎‏‏‎‎‎To listen to or translate text on your screen, choose a digital assistant app in Settings‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‏‏‎‎Change your assistant to use this feature‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‎‏‏‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‎‎‏‏‎‎‏‎‏‎‎‎To listen to or translate text on your screen, change your digital assistant app in Settings‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‎‎‏‎‎‎‎‎‎‏‎‎‏‏‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‎Tap here to listen to text on this screen‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‎Tap here to translate text on this screen‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‎‏‎‎‏‏‎‎‎‏‎‏‏‎‎‏‏‎‏‎‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‎‏‏‎‏‏‎‏‎‎‎‎‎This app can’t be shared‎‏‎‎‏‎" + diff --git a/go/quickstep/res/values-es-rUS/strings.xml b/go/quickstep/res/values-es-rUS/strings.xml new file mode 100644 index 0000000000..0f53967144 --- /dev/null +++ b/go/quickstep/res/values-es-rUS/strings.xml @@ -0,0 +1,20 @@ + + + "Compartir app" + "Escuchar" + "Traducir" + "Lens" + "ENTENDIDO" + "CANCELAR" + "CONFIGURACIÓN" + "Traduce o escucha el texto que aparece en la pantalla" + "Es posible que se comparta información con Google, como el texto de la pantalla, direcciones web y capturas de pantalla.\n\nPara cambiar la información que se comparte, ve a ""Configuración > Apps > Apps predeterminadas > App de asistente digital""." + "Elige un asistente para usar esta función" + "Para escuchar o traducir texto en la pantalla, elige una app de asistente digital en Configuración" + "Cambia el asistente para usar esta función" + "Para escuchar o traducir texto en la pantalla, cambia la app de asistente digital en Configuración" + "Presiona aquí para escuchar texto en esta pantalla" + "Presiona aquí para traducir texto en esta pantalla" + "No se puede compartir esta app" + diff --git a/go/quickstep/res/values-es/strings.xml b/go/quickstep/res/values-es/strings.xml new file mode 100644 index 0000000000..fcd0fb81f8 --- /dev/null +++ b/go/quickstep/res/values-es/strings.xml @@ -0,0 +1,20 @@ + + + "Compartir aplicación" + "Escuchar" + "Traducir" + "Lens" + "ENTENDIDO" + "CANCELAR" + "AJUSTES" + "Traduce o escucha texto que haya en pantalla" + "Se puede compartir con Google cierta información, como el texto que aparece en pantalla, direcciones web o capturas de pantalla.\n\nPara cambiar la información que compartes, ve a ""Ajustes > Aplicaciones > Aplicaciones predeterminadas > Asistente digital""." + "Elige un asistente para usar esta función" + "Para escuchar o traducir texto que haya en tu pantalla, elige una aplicación de asistente digital en Ajustes" + "Cambia tu asistente para usar esta función" + "Para escuchar o traducir texto que haya en tu pantalla, cambia tu aplicación de asistente digital en Ajustes" + "Toca aquí para escuchar el texto que hay en esta pantalla" + "Toca aquí para traducir el texto que hay en esta pantalla" + "Esta aplicación no se puede compartir" + diff --git a/go/quickstep/res/values-et/strings.xml b/go/quickstep/res/values-et/strings.xml new file mode 100644 index 0000000000..2fffdd8395 --- /dev/null +++ b/go/quickstep/res/values-et/strings.xml @@ -0,0 +1,20 @@ + + + "Jaga rakendust" + "Kuula" + "Tõlge" + "Lens" + "SELGE" + "TÜHISTA" + "SEADED" + "Ekraanil oleva teksti tõlkimine või kuulamine" + "Teavet, nagu teie ekraanil olev tekst, veebiaadressid ja ekraanipildid, võidakse jagada Google\'iga.\n\nKui soovite muuta, millist teavet jagate, avage ""Seaded > Rakendused > Vaikerakendused > Digitaalse assistendi rakendus""." + "Valige selle funktsiooni kasutamiseks assistent" + "Ekraanil kuvatud teksti kuulamiseks või tõlkimiseks valige seadetes digitaalse assistendi rakendus" + "Vahetage selle funktsiooni kasutamiseks assistenti" + "Ekraanil kuvatud teksti kuulamiseks või tõlkimiseks vahetage seadetes digitaalse assistendi rakendust" + "Puudutage siin, et ekraanil kuvatud teksti kuulda" + "Puudutage siin, et ekraanil kuvatud tekst tõlkida" + "Seda rakendust ei saa jagada" + diff --git a/go/quickstep/res/values-eu/strings.xml b/go/quickstep/res/values-eu/strings.xml new file mode 100644 index 0000000000..75ddb46c71 --- /dev/null +++ b/go/quickstep/res/values-eu/strings.xml @@ -0,0 +1,20 @@ + + + "Partekatu aplikazioa" + "Entzun" + "Itzuli" + "Lens" + "ADOS" + "UTZI" + "EZARPENAK" + "Itzuli edo entzun pantailako testua" + "Agian pantailako testua, web-helbideak, pantaila-argazkiak eta antzeko informazioa partekatuko duzu Google-rekin.\n\nPartekatzen duzun informazioa aldatzeko, joan hona: ""Ezarpenak > Aplikazioak > Aplikazio lehenetsiak > Laguntzaile digitalaren aplikazioa""." + "Hautatu laguntzaile bat eginbidea erabiltzeko" + "Pantailako testua entzun edo itzultzeko, aukeratu laguntzaile digitalaren aplikazio bat Ezarpenak atalean" + "Aldatu laguntzailea eginbidea erabiltzeko" + "Pantailako testua entzun edo itzultzeko, aldatu laguntzaile digitalaren aplikazioa Ezarpenak atalean" + "Sakatu hau pantailako testua entzuteko" + "Sakatu hau pantailako testua itzultzeko" + "Ezin da partekatu aplikazioa" + diff --git a/go/quickstep/res/values-fa/strings.xml b/go/quickstep/res/values-fa/strings.xml new file mode 100644 index 0000000000..47786e92eb --- /dev/null +++ b/go/quickstep/res/values-fa/strings.xml @@ -0,0 +1,20 @@ + + + "هم‌رسانی برنامه" + "گوش دادن" + "ترجمه" + "لنز" + "متوجه‌ام" + "لغو" + "تنظیمات" + "ترجمه نوشتار روی صفحه‌نمایش یا گوش دادن به آن" + "‏ممکن است اطلاعاتی مثل نوشتار روی صفحه‌نمایش، نشانی‌های وب، و نماگرفت‌ها با Google هم‌رسانی شود.\n\nبرای تغییر اطلاعاتی که هم‌رسانی می‌کنید، به ""تنظیمات < برنامه‌ها < برنامه‌های پیش‌فرض < برنامه دستیار دیجیتال"" بروید." + "برای استفاده از این ویژگی، دستیاری انتخاب کنید" + "برای گوش کردن به نوشتار در صفحه‌نمایش‌تان یا ترجمه کردن آن، یکی از برنامه‌های دستیار دیجیتالی را در «تنظیمات» انتخاب کنید" + "برای استفاده از این ویژگی، دستیارتان را تغییر دهید" + "برای گوش کردن به نوشتار در صفحه‌نمایش‌تان یا ترجمه کردن آن، برنامه دستیار دیجیتالی‌تان را در «تنظیمات» تغییر دهید" + "برای گوش کردن به نوشتار در این صفحه، اینجا ضربه بزنید" + "برای ترجمه نوشتار در این صفحه، اینجا ضربه بزنید" + "نمی‌توان این برنامه را هم‌رسانی کرد" + diff --git a/go/quickstep/res/values-fi/strings.xml b/go/quickstep/res/values-fi/strings.xml new file mode 100644 index 0000000000..bab635f3e8 --- /dev/null +++ b/go/quickstep/res/values-fi/strings.xml @@ -0,0 +1,20 @@ + + + "Jaa sovellus" + "Kuuntele" + "Käännä" + "Lens" + "OK" + "PERU" + "ASETUKSET" + "Käännä tai kuuntele näytöllä oleva teksti" + "Googlelle saatetaan jakaa näytöllä olevaa tekstiä, verkko-osoitteita, kuvakaappauksia ja muita tietoja.\n\nVoit valita jaettavat tiedot valitsemalla ""Asetukset > Sovellukset > Oletussovellukset > Digiavustajasovellus""." + "Valitse avustaja tämän ominaisuuden käyttöön" + "Jos haluat kuunnella tai kääntää näytöllä näkyvää tekstiä, valitse digiavustajasovellus asetuksista" + "Vaihda avustaja tämän ominaisuuden käyttöä varten" + "Jos haluat kuunnella tai kääntää näytöllä näkyvää tekstiä, vaihda digiavustajasovellus asetuksista" + "Kuuntele näytöllä näkyvä teksti napauttamalla tästä" + "Käännä näytöllä näkyvä teksti napauttamalla tästä" + "Tätä sovellusta ei voi jakaa" + diff --git a/go/quickstep/res/values-fr-rCA/strings.xml b/go/quickstep/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000000..2cc9d8fe7c --- /dev/null +++ b/go/quickstep/res/values-fr-rCA/strings.xml @@ -0,0 +1,20 @@ + + + "Partager application" + "Écouter" + "Traduire" + "Lentille" + "OK" + "ANNULER" + "PARAMÈTRES" + "Traduire ou écouter le texte à l\'écran" + "Des renseignements comme du texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagés avec Google.\n\nPour modifier les renseignements que vous partagez, accédez à ""Paramètres > Applications > Applications par défaut > Application d\'assistant numérique""." + "Choisir un assistant pour utiliser cette fonctionnalité" + "Pour écouter ou traduire le texte affiché sur votre écran, choisissez l\'application d\'un assistant numérique dans les paramètres" + "Modifier votre assistant pour utiliser cette fonctionnalité" + "Pour écouter ou traduire le texte affiché sur votre écran, modifiez l\'application de votre assistant numérique dans les paramètres" + "Touchez ce bouton pour écouter le texte affiché sur cet écran" + "Touchez ce bouton pour traduire le texte affiché sur cet écran" + "Cette application ne peut pas être partagée" + diff --git a/go/quickstep/res/values-fr/strings.xml b/go/quickstep/res/values-fr/strings.xml new file mode 100644 index 0000000000..fded0af535 --- /dev/null +++ b/go/quickstep/res/values-fr/strings.xml @@ -0,0 +1,20 @@ + + + "Partager l\'appli" + "Écouter" + "Traduire" + "Lens" + "OK" + "ANNULER" + "PARAMÈTRES" + "Traduire ou écouter le texte à l\'écran" + "Des informations comme le texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagées avec Google.\n\nPour modifier les types d\'informations que vous partagez, accédez à ""Paramètres > Applis > Applis par défaut > Appli d\'assistant numérique""." + "Sélectionnez un assistant pour utiliser cette fonctionnalité" + "Pour écouter ou traduire le texte à l\'écran, sélectionnez une appli d\'assistant numérique dans \"Paramètres\"" + "Changez d\'assistant pour utiliser cette fonctionnalité" + "Pour écouter ou traduire le texte à l\'écran, modifiez l\'appli d\'assistant numérique dans \"Paramètres\"" + "Appuyez ici pour écouter le texte à l\'écran" + "Appuyez ici pour traduire le texte à l\'écran" + "Impossible de partager cette appli" + diff --git a/go/quickstep/res/values-gl/strings.xml b/go/quickstep/res/values-gl/strings.xml new file mode 100644 index 0000000000..86c6a57166 --- /dev/null +++ b/go/quickstep/res/values-gl/strings.xml @@ -0,0 +1,20 @@ + + + "Compartir aplicación" + "Escoitar" + "Traducir" + "Lens" + "ENTENDIDO" + "CANCELAR" + "CONFIGURACIÓN" + "Traducir ou escoitar o texto que aparece na pantalla" + "É posible que se comparta con Google información como o texto que aparece na pantalla, os enderezos web e as capturas de pantalla.\n\nPara cambiar os datos que se comparten, vai a ""Configuración > Aplicacións > Aplicacións predeterminadas > Aplicación de asistente dixital""." + "Escolle un asistente para utilizar esta función" + "Para escoitar ou traducir o texto da pantalla, escolle unha aplicación de asistente dixital en Configuración" + "Cambia o asistente para utilizar esta función" + "Para escoitar ou traducir o texto da pantalla, cambia a aplicación de asistente dixital en Configuración" + "Tocar aquí para escoitar o texto desta pantalla" + "Tocar aquí para traducir o texto desta pantalla" + "Non se pode compartir esta aplicación" + diff --git a/go/quickstep/res/values-gu/strings.xml b/go/quickstep/res/values-gu/strings.xml index 9cd1101e7c..80d9e2ec30 100644 --- a/go/quickstep/res/values-gu/strings.xml +++ b/go/quickstep/res/values-gu/strings.xml @@ -16,4 +16,5 @@ "તમારી સ્ક્રીન પર ટેક્સ્ટ સાંભળવા માટે અથવા તેનો અનુવાદ કરવા માટે, સેટિંગમાં જઈને તમારી ડિજિટલ આસિસ્ટંટ ઍપ બદલો" "આ સ્ક્રીન પર ટેક્સ્ટ સાંભળવા માટે અહીં ટૅપ કરો" "આ સ્ક્રીન પર ટેક્સ્ટનો અનુવાદ કરવા માટે અહીં ટૅપ કરો" + "આ ઍપ શેર કરી શકાતી નથી" diff --git a/go/quickstep/res/values-hi/strings.xml b/go/quickstep/res/values-hi/strings.xml new file mode 100644 index 0000000000..ecf0cfb8a6 --- /dev/null +++ b/go/quickstep/res/values-hi/strings.xml @@ -0,0 +1,20 @@ + + + "ऐप्लिकेशन शेयर करें" + "सुनें" + "अनुवाद करें" + "Google Lens" + "ठीक है" + "रद्द करें" + "सेटिंग" + "स्क्रीन पर मौजूद टेक्स्ट का अनुवाद पाएं या उसे सुनें" + "आपकी स्क्रीन पर मौजूद टेक्स्ट, वेब पते, और स्क्रीनशॉट जैसी जानकारी Google के साथ शेयर की जा सकती है.\n\nआप Google के साथ किस जानकारी को शेयर करते हैं, इसे कंट्रोल करने के लिए ""सेटिंग > ऐप्लिकेशन > डिफ़ॉल्ट ऐप्लिकेशन > डिजिटल असिस्टेंट ऐप्लिकेशन"" पर जाएं." + "इस सुविधा का इस्तेमाल करने के लिए, कोई डिजिटल असिस्टेंट ऐप्लिकेशन चुनें" + "स्क्रीन पर मौजूद टेक्स्ट का अनुवाद करने या उसे सुनने के लिए, \'सेटिंग\' में जाकर कोई डिजिटल असिस्टेंट ऐप्लिकेशन चुनें" + "इस सुविधा का इस्तेमाल करने के लिए, अपना डिजिटल असिस्टेंट ऐप्लिकेशन बदलें" + "स्क्रीन पर मौजूद टेक्स्ट का अनुवाद करने या उसे सुनने के लिए, \'सेटिंग\' में जाकर अपना डिजिटल असिस्टेंट ऐप्लिकेशन बदलें" + "स्क्रीन पर मौजूद टेक्स्ट को सुनने के लिए, यहां टैप करें" + "स्क्रीन पर मौजूद टेक्स्ट का अनुवाद करने के लिए, यहां टैप करें" + "इस ऐप्लिकेशन को शेयर नहीं किया जा सकता" + diff --git a/go/quickstep/res/values-hr/strings.xml b/go/quickstep/res/values-hr/strings.xml new file mode 100644 index 0000000000..137472d20c --- /dev/null +++ b/go/quickstep/res/values-hr/strings.xml @@ -0,0 +1,20 @@ + + + "Dijeli aplikaciju" + "Slušajte" + "Prevedi" + "Objektiv" + "SHVAĆAM" + "ODUSTANI" + "POSTAVKE" + "Prevedite ili slušajte tekst na zaslonu" + "Informacije kao što su tekst na vašem zaslonu, web-adrese i snimke zaslona mogu se dijeliti s Googleom.\n\nDa biste promijenili informacije koje dijelite, otvorite ""Postavke > Aplikacije > Zadane aplikacije > Aplikacija digitalnog asistenta""." + "Odaberite asistenta za upotrebu te značajke" + "Da biste poslušali ili preveli tekst na zaslonu, odaberite aplikaciju digitalnog asistenta u postavkama" + "Promijenite asistenta da biste koristili tu značajku" + "Da biste poslušali ili preveli tekst na zaslonu, promijenite aplikaciju digitalnog asistenta u postavkama" + "Dodirnite ovdje da biste poslušali tekst na ovom zaslonu" + "Dodirnite ovdje da biste preveli tekst na ovom zaslonu" + "Ovu aplikaciju ne možete dijeliti" + diff --git a/go/quickstep/res/values-hu/strings.xml b/go/quickstep/res/values-hu/strings.xml new file mode 100644 index 0000000000..d94e6b72a7 --- /dev/null +++ b/go/quickstep/res/values-hu/strings.xml @@ -0,0 +1,20 @@ + + + "App megosztása" + "Lejátszás" + "Fordítás" + "Lens" + "ÉRTEM" + "MÉGSE" + "BEÁLLÍTÁSOK" + "Képernyőn megjelenő szöveg fordítása és hallgatása" + "Előfordulhat, hogy a rendszer megoszt bizonyos adatokat (például képernyőn megjelenő szöveget, internetcímeket és képernyőképeket) a Google-lal.\n\nHa módosítani szeretné, hogy milyen adatokat oszt meg, lépjen a ""Beállítások és alkalmazások; Alapértelmezett alkalmazások; Digitális asszisztens app"" menüpontra." + "Válasszon asszisztenst a funkció használatához" + "A képernyőn megjelenő szöveg meghallgatásához vagy lefordításához válasszon digitálisasszisztens-alkalmazást a Beállítások menüben" + "Módosítsa az asszisztenst a funkció használatához" + "A képernyőn megjelenő szöveg meghallgatásához vagy lefordításához módosítsa a digitálisasszisztens-alkalmazást a Beállítások menüben" + "Koppintson ide a jelenleg képernyőn lévő szöveg meghallgatásához" + "Koppintson ide a jelenleg képernyőn lévő szöveg lefordításához" + "Ezt az alkalmazást nem lehet megosztani" + diff --git a/go/quickstep/res/values-hy/strings.xml b/go/quickstep/res/values-hy/strings.xml new file mode 100644 index 0000000000..c6352c9387 --- /dev/null +++ b/go/quickstep/res/values-hy/strings.xml @@ -0,0 +1,20 @@ + + + "Կիսվել հավելվածով" + "Լսել" + "Թարգմանել" + "Տեսապակի" + "ԵՂԱՎ" + "ՉԵՂԱՐԿԵԼ" + "ԿԱՐԳԱՎՈՐՈՒՄՆԵՐ" + "Էկրանի տեքստի թարգմանություն կամ ունկնդրում" + "Որոշակի տեղեկություններ (օր․՝ էկրանի տեքստը, վեբ հասցեները և սքրինշոթները) կարող են ուղարկվել Google-ին։\n\nՏեղեկությունները, որոնցով դուք կիսվում եք, կարող եք փոխել՝ անցնելով ""Կարգավորումներ > Հավելվածներ > Կանխադրված հավելվածներ > Թվային օգնականի հավելված""։" + "Այս գործառույթն օգտագործելու համար ընտրեք օգնական" + "Էկրանի տեքստը լսելու կամ թարգմանելու համար կարգավորումներում ընտրեք թվային օգնականի հավելված։" + "Այս գործառույթն օգտագործելու համար փոխեք ձեր օգնականը" + "Էկրանի տեքստը լսելու կամ թարգմանելու համար կարգավորումներում փոխեք ձեր թվային օգնականի հավելվածը։" + "Հպեք այստեղ՝ այս էկրանի տեքստը լսելու համար" + "Հպեք այստեղ` այս էկրանի տեքստը թարգմանելու համար" + "Հնարավոր չէ կիսվել այս հավելվածով" + diff --git a/go/quickstep/res/values-in/strings.xml b/go/quickstep/res/values-in/strings.xml new file mode 100644 index 0000000000..692bedc57f --- /dev/null +++ b/go/quickstep/res/values-in/strings.xml @@ -0,0 +1,20 @@ + + + "Bagikan Aplikasi" + "Dengarkan" + "Terjemahkan" + "Lens" + "OKE" + "BATAL" + "SETELAN" + "Terjemahkan atau dengarkan teks di layar" + "Informasi seperti teks di layar, alamat web, dan screenshot dapat dibagikan ke Google.\n\nUntuk mengubah informasi yang Anda bagikan, buka ""Setelan > Aplikasi > Aplikasi default > Aplikasi asisten digital""." + "Pilih asisten untuk menggunakan fitur ini" + "Untuk mendengarkan atau menerjemahkan teks di layar, pilih aplikasi asisten digital di Setelan" + "Ubah asisten untuk menggunakan fitur ini" + "Untuk mendengarkan atau menerjemahkan teks di layar, ubah aplikasi asisten digital Anda di Setelan" + "Ketuk di sini untuk mendengarkan teks di layar ini" + "Ketuk di sini untuk menerjemahkan teks di layar ini" + "Aplikasi ini tidak dapat dibagikan" + diff --git a/go/quickstep/res/values-is/strings.xml b/go/quickstep/res/values-is/strings.xml new file mode 100644 index 0000000000..25a96dd4ba --- /dev/null +++ b/go/quickstep/res/values-is/strings.xml @@ -0,0 +1,20 @@ + + + "Deila forriti" + "Hlusta" + "Þýða" + "Linsa" + "ÉG SKIL" + "HÆTTA VIÐ" + "STILLINGAR" + "Þýða eða hlusta á texta á skjánum" + "Mögulegt er að upplýsingum á borð við texta á skjánum, vefslóðum og skjámyndum verði deilt með Google.\n\nTil að breyta því hvaða upplýsingum þú deilir skaltu opna ""Stillingar > Forrit > Sjálfgefin forrit > Forrit stafræns hjálpara""." + "Veldu hjálpara til að nota þennan eiginleika" + "Veldu stafrænan hjálpara í stillingum til að hlusta á eða þýða texta á skjánum" + "Breyttu hjálparanum til að nota þennan eiginleika" + "Breyttu forriti stafræna hjálparans í stillingum til að hlusta á eða þýða texta" + "Ýttu hér til að hlusta á texta á þessum skjá" + "Ýttu hér til að þýða texta á þessum skjá" + "Ekki er hægt að deila þessu forriti" + diff --git a/go/quickstep/res/values-it/strings.xml b/go/quickstep/res/values-it/strings.xml new file mode 100644 index 0000000000..ebdea202fa --- /dev/null +++ b/go/quickstep/res/values-it/strings.xml @@ -0,0 +1,20 @@ + + + "Condividi l\'app" + "Ascolta" + "Traduttore" + "Lens" + "OK" + "ANNULLA" + "IMPOSTAZIONI" + "Traduci o ascolta il testo sullo schermo" + "Alcune informazioni, come il testo sullo schermo, gli indirizzi web e gli screenshot, potrebbero essere condivise con Google.\n\nPer modificare quali informazioni condividere, vai a ""Impostazioni > App > App predefinite > App assistente digitale""." + "Scegli un assistente per usare questa funzionalità" + "Per ascoltare o tradurre il testo mostrato sullo schermo, scegli l\'app di un assistente digitale nelle Impostazioni" + "Cambia l\'assistente per usare questa funzionalità" + "Per ascoltare o tradurre il testo mostrato sullo schermo, cambia l\'app dell\'assistente digitale nelle Impostazioni" + "Tocca qui per ascoltare il testo mostrato in questa schermata" + "Tocca qui per tradurre il testo mostrato in questa schermata" + "Impossibile condividere questa app" + diff --git a/go/quickstep/res/values-iw/strings.xml b/go/quickstep/res/values-iw/strings.xml new file mode 100644 index 0000000000..ddb8ddd9a9 --- /dev/null +++ b/go/quickstep/res/values-iw/strings.xml @@ -0,0 +1,20 @@ + + + "לשיתוף האפליקציה" + "האזנה" + "תרגום" + "Lens" + "הבנתי" + "ביטול" + "הגדרות" + "תרגום טקסט שמוצג במסך או האזנה לו" + "‏ייתכן שישותף עם Google מידע כגון טקסט שמוצג במסך, כתובות אינטרנט וצילומי מסך.\n\nכדי לקבוע איזה מידע ישותף, יש לעבור אל ""הגדרות > אפליקציות &gt אפליקציות ברירת מחדל > אפליקציית עוזר דיגיטלי""." + "יש לבחור עוזר דיגיטלי כדי להשתמש בתכונה הזו" + "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לבחור אפליקציית עוזר דיגיטלי ב\'הגדרות\'" + "צריך לשנות את העוזר הדיגיטלי כדי להשתמש בתכונה הזו" + "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לשנות את אפליקציית העוזר הדיגיטלי ב\'הגדרות\'" + "צריך להקיש כאן כדי להאזין לטקסט שבמסך הזה" + "צריך להקיש כאן כדי לתרגם את הטקסט שבמסך הזה" + "אי אפשר לשתף את האפליקציה הזו" + diff --git a/go/quickstep/res/values-ja/strings.xml b/go/quickstep/res/values-ja/strings.xml new file mode 100644 index 0000000000..3ce87f7145 --- /dev/null +++ b/go/quickstep/res/values-ja/strings.xml @@ -0,0 +1,20 @@ + + + "アプリを共有" + "聴く" + "翻訳" + "レンズ" + "OK" + "キャンセル" + "設定" + "画面上のテキストを翻訳または聞く" + "画面上のテキスト、ウェブアドレス、スクリーンショットなどの情報が Google と共有される場合があります。\n\n共有される情報を変更するには、""[設定] > [アプリ] > [デフォルトのアプリ] > [デジタル アシスタント アプリ]"" に移動してください。" + "この機能を使用するにはアシスタントを選択してください" + "画面上のテキストを翻訳するかまたは聞くには、[設定] でデジタル アシスタント アプリを選択してください" + "この機能を使用するにはアシスタントを変更してください" + "画面上のテキストを翻訳するかまたは聞くには、[設定] でデジタル アシスタント アプリを変更してください" + "この画面上のテキストを聞くには、ここをタップしてください" + "この画面上のテキストを翻訳するには、ここをタップしてください" + "このアプリは共有できません" + diff --git a/go/quickstep/res/values-ka/strings.xml b/go/quickstep/res/values-ka/strings.xml new file mode 100644 index 0000000000..0f7381040b --- /dev/null +++ b/go/quickstep/res/values-ka/strings.xml @@ -0,0 +1,20 @@ + + + "აპის გაზიარება" + "მოსმენა" + "თარგმნა" + "Lens" + "გასაგებია" + "გაუქმება" + "პარამეტრები" + "თარგმნეთ ან მოისმინეთ ეკრანზე ნაჩვენები ტექსტი" + "ისეთი ინფორმაცია, როგორიც არის ტექსტი თქვენს ეკრანზე, ვებ-მისამართები და ეკრანის ანაბეჭდები შეიძლება გაზიარდეს Google-თან.\n\nთუ გსურთ, შეცვალოთ, რა ინფორმაციას აზიარებთ, გადადით: ""პარამეტრები > აპები > ნაგულისხმევი აპები > ციფრული ასისტენტის აპი""." + "ამ ფუნქციით სარგებლობისთვის აირჩიეთ ასისტენტი" + "თქვენს ეკრანზე ნაჩვენები ტექსტის მოსასმენად ან სათარგმნად, აირჩიეთ ციფრული ასისტენტის აპი პარამეტრებიდან" + "ამ ფუნქციით სარგებლობისთვის შეცვალეთ ასისტენტი" + "თქვენს ეკრანზე ნაჩვენები ტექსტის მოსასმენად ან სათარგმნად, შეცვალეთ ციფრული ასისტენტის აპი პარამეტრებიდან" + "შეეხეთ აქ ამ ეკრანზე ნაჩვენები ტექსტის მოსასმენად" + "შეეხეთ აქ ამ ეკრანზე ნაჩვენები ტექსტის სათარგმნად" + "ამ აპის გაზიარება შეუძლებელია" + diff --git a/go/quickstep/res/values-kk/strings.xml b/go/quickstep/res/values-kk/strings.xml index 9720e2ab0a..2a2f96db54 100644 --- a/go/quickstep/res/values-kk/strings.xml +++ b/go/quickstep/res/values-kk/strings.xml @@ -16,4 +16,5 @@ "Экрандағы мәтінді тыңдау немесе аудару үшін параметрлерден цифрлық көмекшіні өзгертіңіз." "Экрандағы мәтінді тыңдау үшін осы жерде түртіңіз." "Экрандағы мәтінді аудару үшін осы жерде түртіңіз." + "Бұл қолданбаны бөлісу мүмкін емес." diff --git a/go/quickstep/res/values-km/strings.xml b/go/quickstep/res/values-km/strings.xml new file mode 100644 index 0000000000..cec764668f --- /dev/null +++ b/go/quickstep/res/values-km/strings.xml @@ -0,0 +1,20 @@ + + + "ចែក​រំលែក​កម្មវិធី" + "ស្តាប់" + "បកប្រែ" + "Lens" + "យល់ហើយ" + "បោះបង់" + "ការកំណត់" + "បកប្រែ ឬស្ដាប់អត្ថបទ​នៅលើអេក្រង់" + "ព័ត៌មានដូចជា អត្ថបទនៅលើ​អេក្រង់របស់អ្នក អាសយដ្ឋានទំព័រ និងរូបថតអេក្រង់​​អាចនឹងត្រូវបានចែក​រំលែកជាមួយ Google។\n\nដើម្បីប្ដូរព័ត៌មាន​ដែលអ្នកចែករំលែក សូមចូលទៅកាន់""ការកំណត់ > កម្មវិធី > កម្មវិធីលំនាំដើម > កម្មវិធីជំនួយការឌីជីថល""។" + "ជ្រើសរើស​ជំនួយការ ដើម្បីប្រើ​មុខងារនេះ" + "ដើម្បីស្ដាប់ ឬបកប្រែ​អត្ថបទ​នៅលើ​អេក្រង់របស់អ្នក សូមជ្រើសរើស​កម្មវិធី​ជំនួយការឌីជីថល​នៅក្នុង​ការកំណត់" + "ប្ដូរជំនួយការ​របស់អ្នក ដើម្បីប្រើ​មុខងារនេះ" + "ដើម្បីស្ដាប់ ឬបកប្រែ​អត្ថបទ​នៅលើ​អេក្រង់របស់អ្នក សូមប្ដូរ​កម្មវិធី​ជំនួយការឌីជីថល​របស់អ្នក​នៅក្នុង​ការកំណត់" + "ចុចត្រង់នេះ ដើម្បីស្ដាប់​អត្ថបទ​នៅលើ​អេក្រង់នេះ" + "ចុចត្រង់នេះ ដើម្បីបកប្រែ​អត្ថបទ​នៅលើ​អេក្រង់នេះ" + "មិនអាចចែករំលែកកម្មវិធីនេះបានទេ" + diff --git a/go/quickstep/res/values-kn/strings.xml b/go/quickstep/res/values-kn/strings.xml index ef1954199b..28ba66b1ee 100644 --- a/go/quickstep/res/values-kn/strings.xml +++ b/go/quickstep/res/values-kn/strings.xml @@ -16,4 +16,5 @@ "ನಿಮ್ಮ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿರುವ ಪಠ್ಯವನ್ನು ಆಲಿಸಲು ಅಥವಾ ಅನುವಾದಿಸಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನಿಮ್ಮ ಡಿಜಿಟಲ್ ಅಸಿಸ್ಟೆಂಟ್ ಆ್ಯಪ್ ಅನ್ನು ಬದಲಾಯಿಸಿ" "ಈ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿರುವ ಪಠ್ಯವನ್ನು ಆಲಿಸಲು ಇಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡಿ" "ಈ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿರುವ ಪಠ್ಯವನ್ನು ಅನುವಾದಿಸಲು ಇಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡಿ" + "ಈ ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಸಾಧ್ಯವಿಲ್ಲ" diff --git a/go/quickstep/res/values-ko/strings.xml b/go/quickstep/res/values-ko/strings.xml new file mode 100644 index 0000000000..97ccfefb7a --- /dev/null +++ b/go/quickstep/res/values-ko/strings.xml @@ -0,0 +1,20 @@ + + + "앱 공유" + "듣기" + "번역" + "렌즈" + "확인" + "취소" + "설정" + "화면의 텍스트 번역 또는 듣기" + "화면의 텍스트, 웹 주소, 스크린샷과 같은 정보가 Google과 공유될 수 있습니다.\n\n공유되는 정보를 변경하려면 ""설정 > 앱 > 기본 앱 > 디지털 어시스턴트 앱""으로 이동하세요." + "이 기능을 사용하려면 어시스턴트를 선택하세요." + "화면에서 텍스트를 듣거나 번역하려면 설정에서 디지털 어시스턴트 앱을 선택하세요." + "어시스턴트가 이 기능을 사용하도록 변경하세요." + "화면에서 텍스트를 듣거나 번역하려면 설정에서 디지털 어시스턴트 앱을 변경하세요." + "화면에서 텍스트를 들으려면 여기를 탭하세요." + "화면에서 텍스트를 번역하려면 여기를 탭하세요." + "공유할 수 없는 앱입니다." + diff --git a/go/quickstep/res/values-ky/strings.xml b/go/quickstep/res/values-ky/strings.xml new file mode 100644 index 0000000000..dcc1e4ea77 --- /dev/null +++ b/go/quickstep/res/values-ky/strings.xml @@ -0,0 +1,20 @@ + + + "Колдонмону бөлүшүү" + "Угуу" + "Которуу" + "Lens" + "ТҮШҮНДҮМ" + "ЖОККО ЧЫГАРУУ" + "ЖӨНДӨӨЛӨР" + "Экрандагы текстти которуу же угуу" + "Экрандагы текст, веб-даректер жана скриншоттор сыяктуу маалымат Google менен бөлүшүлүшү мүмкүн.\n\nБөлүшүлгөн маалыматты өзгөртүү үчүн""Параметрлер > Колдонмолор > Демейки колдонмолор > Санариптик жардамчы колдонмосуна өтүңүз""." + "Бул функцияны колдонуу үчүн жардамчыны тандаңыз" + "Экраныңыздагы текстти угуу же которуу үчүн Параметрлерден санариптик жардамчы колдонмосун тандаңыз" + "Бул функцияны колдонуу үчүн жардамчыңызды өзгөртүңүз" + "Экраныңыздагы текстти угуу же которуу үчүн Параметрлерден санариптик жардамчы колдонмосун өзгөртүңүз" + "Бул экрандагы текстти угуу үчүн бул жерди басыңыз" + "Бул экрандагы текстти которуу үчүн бул жерди басыңыз" + "Бул колдонмону бөлүшүүгө болбойт" + diff --git a/go/quickstep/res/values-lo/strings.xml b/go/quickstep/res/values-lo/strings.xml new file mode 100644 index 0000000000..b6f7b076ba --- /dev/null +++ b/go/quickstep/res/values-lo/strings.xml @@ -0,0 +1,20 @@ + + + "ແບ່ງປັນແອັບ" + "ຟັງ" + "ແປພາສາ" + "Lens" + "ເຂົ້າໃຈແລ້ວ" + "ຍົກເລີກ" + "ການຕັ້ງຄ່າ" + "ແປພາສາ ຫຼື ຟັງຂໍ້ຄວາມຢູ່ໜ້າຈໍ" + "ຂໍ້ມູນ ເຊັ່ນ: ຂໍ້ຄວາມຢູ່ໜ້າຈໍຂອງທ່ານ, ທີ່ຢູ່ເວັບ ແລະ ຮູບໜ້າຈໍອາດຖືກແບ່ງປັນໃຫ້ກັບ Google.\n\nເພື່ອປ່ຽນແປງຂໍ້ມູນທີ່ທ່ານແບ່ງປັນ, ໃຫ້ເຂົ້າໄປ ""ການຕັ້ງຄ່າ > ແອັບ > ແອັບເລີ່ມຕົ້ນ > ແອັບຜູ້ຊ່ວຍດິຈິຕອນ""." + "ເລືອກຜູ້ຊ່ວຍເພື່ອໃຊ້ຄຸນສົມບັດນີ້" + "ເພື່ອຟັງ ຫຼື ແປຂໍ້ຄວາມຢູ່ໜ້າຈໍຂອງທ່ານ, ໃຫ້ເລືອກຜູ້ຊ່ວຍດິຈິຕອນໃດໜຶ່ງໃນການຕັ້ງຄ່າ" + "ປ່ຽນຜູ້ຊ່ວຍຂອງທ່ານເພື່ອໃຊ້ຄຸນສົມບັດນີ້" + "ເພື່ອຟັງ ຫຼື ແປຂໍ້ຄວາມຢູ່ໜ້າຈໍຂອງທ່ານ, ໃຫ້ປ່ຽນຜູ້ຊ່ວຍດິຈິຕອນຂອງທ່ານໃນການຕັ້ງຄ່າ" + "ແຕະບ່ອນນີ້ເພື່ອຟັງຂໍ້ຄວາມຢູ່ໜ້າຈໍນີ້" + "ແຕະບ່ອນນີ້ເພື່ອແປຂໍ້ຄວາມຢູ່ໜ້າຈໍນີ້" + "ບໍ່ສາມາດແບ່ງປັນແອັບນີ້ໄດ້" + diff --git a/go/quickstep/res/values-lt/strings.xml b/go/quickstep/res/values-lt/strings.xml new file mode 100644 index 0000000000..dffe34f689 --- /dev/null +++ b/go/quickstep/res/values-lt/strings.xml @@ -0,0 +1,20 @@ + + + "Bendrinti programą" + "Klausyti" + "Išversti" + "Lens" + "SUPRATAU" + "ATŠAUKTI" + "NUSTATYMAI" + "Ekrane esančio teksto vertimas arba klausymas" + "Tam tikra informacija, pvz., ekrane esantis tekstas, žiniatinklio adresai ir ekrano kopijos, gali būti bendrinama su „Google“.\n\nJei norite pakeisti, kokia informacija gali būti bendrinama, eikite į ""„Nustatymai“ > „Programos“ > „Numatytosios programos“ > „Skaitmeninio pagelbiklio programa“""." + "Norint naudoti šią funkciją, reikia pasirinkti pagelbiklį" + "Jei norite klausyti teksto ekrane ar jį išversti, pasirinkite skaitmeninio pagelbiklio programą „Nustatymų“ skiltyje" + "Norint naudoti šią funkciją, reikia pakeisti pagelbiklį" + "Jei norite klausyti teksto ekrane ar jį išversti, pakeiskite skaitmeninio pagelbiklio programą „Nustatymų“ skiltyje" + "Palieskite čia, jei norite klausyti teksto šiame ekrane" + "Palieskite čia, jei norite išversti tekstą šiame ekrane" + "Šios programos negalima bendrinti" + diff --git a/go/quickstep/res/values-lv/strings.xml b/go/quickstep/res/values-lv/strings.xml new file mode 100644 index 0000000000..faf3274bda --- /dev/null +++ b/go/quickstep/res/values-lv/strings.xml @@ -0,0 +1,20 @@ + + + "Kopīgot lietotni" + "Klausīties" + "Tulkot" + "Lens" + "LABI" + "ATCELT" + "IESTATĪJUMI" + "Ekrānā redzamā teksta atskaņošana vai tulkošana" + "Ar uzņēmumu Google var tikt kopīgota noteikta informācija, piemēram, ekrānā redzamais teksts, tīmekļa adreses un ekrānuzņēmumi.\n\nLai mainītu kopīgotās informācijas veidu, atveriet sadaļu ""Iestatījumi > Lietotnes > Noklusējuma lietotnes > Digitālā asistenta lietotne""." + "Lai izmantotu šo funkciju, izvēlieties asistentu" + "Lai klausītos vai tulkotu ekrānā parādīto tekstu, iestatījumos izvēlieties digitālā asistenta lietotni" + "Lai izmantotu šo funkciju, mainiet asistentu" + "Lai klausītos vai tulkotu ekrānā parādīto tekstu, iestatījumos mainiet digitālā asistenta lietotni" + "Lai klausītos ekrānā parādīto tekstu, pieskarieties šeit" + "Lai tulkotu ekrānā parādīto tekstu, pieskarieties šeit" + "Šo lietotni nevar kopīgot." + diff --git a/go/quickstep/res/values-mk/strings.xml b/go/quickstep/res/values-mk/strings.xml new file mode 100644 index 0000000000..7e8cecf0f6 --- /dev/null +++ b/go/quickstep/res/values-mk/strings.xml @@ -0,0 +1,20 @@ + + + "Сподели апликација" + "Слушај" + "Преведи" + "Lens" + "СФАТИВ" + "ОТКАЖИ" + "ПОСТАВКИ" + "Преведете или слушајте текст на екранот" + "Информациите како текст на екранот, интернет-адреси и слики од екранот може да се споделуваат со Google.\n\nЗа да промените кои информации ќе се споделуваат, одете во ""Поставки > Апликации > Стандардни апликации > Апликација за дигитален помошник""." + "Изберете помошник за да ја користите функцијава" + "За да го слушнете или преведете текстот од екранот, изберете ја апликацијата за дигитален помошник во „Поставки“" + "Променете го помошникот за да ја користите функцијава" + "За да го слушнете или преведете текстот од екранот, променете ја апликацијата за дигитален помошник во „Поставки“" + "Допрете тука за да го слушнете текстот од екранов" + "Допрете тука за да го преведете текстот од екранов" + "Апликацијава не може да се сподели" + diff --git a/go/quickstep/res/values-ml/strings.xml b/go/quickstep/res/values-ml/strings.xml index d34a5f1b3f..ed5739568c 100644 --- a/go/quickstep/res/values-ml/strings.xml +++ b/go/quickstep/res/values-ml/strings.xml @@ -16,4 +16,5 @@ "നിങ്ങളുടെ സ്ക്രീനിലുള്ള ടെക്‌സ്‌റ്റ് കേൾക്കാനോ വിവർത്തനം ചെയ്യാനോ, ക്രമീകരണത്തിലെ നിങ്ങളുടെ ഡിജിറ്റൽ അസിസ്‌റ്റന്റ് ആപ്പ് മാറ്റുക" "ഈ സ്ക്രീനിലെ ടെക്‌സ്‌റ്റ് കേൾക്കാൻ, ഇവിടെ ടാപ്പ് ചെയ്യുക" "ഈ സ്ക്രീനിലെ ടെക്‌സ്‌റ്റ് വിവർത്തനം ചെയ്യാൻ, ഇവിടെ ടാപ്പ് ചെയ്യുക" + "ഈ ആപ്പ് പങ്കിടാനാകില്ല" diff --git a/go/quickstep/res/values-mn/strings.xml b/go/quickstep/res/values-mn/strings.xml index d1144ee648..d03b2d2ad7 100644 --- a/go/quickstep/res/values-mn/strings.xml +++ b/go/quickstep/res/values-mn/strings.xml @@ -16,4 +16,5 @@ "Дэлгэц дээрээ текст сонсох эсвэл орчуулахын тулд Тохиргоо хэсэгт дижитал туслах аппаа өөрчилнө үү" "Энэ дэлгэц дээр текст сонсохын тулд энд товшино уу" "Энэ дэлгэц дээр текст орчуулахын тулд энд товшино уу" + "Энэ аппыг хуваалцах боломжгүй" diff --git a/go/quickstep/res/values-mr/strings.xml b/go/quickstep/res/values-mr/strings.xml new file mode 100644 index 0000000000..d3dff929d0 --- /dev/null +++ b/go/quickstep/res/values-mr/strings.xml @@ -0,0 +1,20 @@ + + + "अ‍ॅप शेअर करा" + "ऐका" + "भाषांतर करा" + "Lens" + "समजले" + "रद्द करा" + "सेटिंग्ज" + "स्क्रीनवरील मजकूर भाषांतरित करा किंवा ऐका" + "तुमच्या स्क्रीनवरील मजकूर, वेब अ‍ॅड्रेस आणि स्क्रीनशॉट यांसारखी माहिती Google सह शेअर केली जाऊ शकते.\n\nतुम्ही कोणती माहिती शेअर करता हे बदलण्यासाठी ""सेटिंग्ज > ॲप्स > डीफॉल्ट ॲप्स > डिजिटल असिस्टंट ॲपवर जा""." + "हे वैशिष्ट्य वापरण्यासाठी तुमचे असिस्टंट निवडा" + "तुमच्या स्क्रीनवरील मजकूर ऐकण्यासाठी किंवा भाषांतर करण्यासाठी, सेटिंग्ज मध्ये तुमचे डिजिटल असिस्टंट अ‍ॅप निवडा" + "हे वैशिष्ट्य वापरण्यासाठी असिस्टंट बदला" + "तुमच्या स्क्रीनवरील मजकूर ऐकण्यासाठी किंवा भाषांतर करण्यासाठी, सेटिंग्ज मध्ये तुमचे डिजिटल असिस्टंट अ‍ॅप बदला" + "या स्क्रीनवरील मजकूर ऐकण्यासाठी येथे टॅप करा" + "या स्क्रीनवरील मजकुराचे भाषांतर करण्यासाठी येथे टॅप करा" + "हे ॲप शेअर केले जाऊ शकत नाही" + diff --git a/go/quickstep/res/values-ms/strings.xml b/go/quickstep/res/values-ms/strings.xml index 9ecbe99133..3b0a7fbe71 100644 --- a/go/quickstep/res/values-ms/strings.xml +++ b/go/quickstep/res/values-ms/strings.xml @@ -16,4 +16,5 @@ "Untuk mendengar atau menterjemahkan teks pada skrin anda, tukar apl pembantu digital anda dalam Tetapan" "Ketik di sini untuk mendengar teks pada skrin ini" "Ketik di sini untuk menterjemahkan teks pada skrin ini" + "Apl ini tidak boleh dikongsi" diff --git a/go/quickstep/res/values-my/strings.xml b/go/quickstep/res/values-my/strings.xml new file mode 100644 index 0000000000..cbb485a6d2 --- /dev/null +++ b/go/quickstep/res/values-my/strings.xml @@ -0,0 +1,20 @@ + + + "အက်ပ် မျှဝေရန်" + "နားထောင်ရန်" + "ဘာသာပြန်ရန်" + "Lens" + "နားလည်ပြီ" + "မလုပ်တော့" + "ဆက်တင်များ" + "ဖန်သားပြင်ပေါ်ရှိ စာသားကို ဘာသာပြန်ပါ (သို့) နားထောင်ပါ" + "သင့်ဖန်သားပြင်ပေါ်ရှိ စာသား၊ ဝဘ်လိပ်စာနှင့် ဖန်သားပြင်ဓာတ်ပုံများကဲ့သို့ အချက်အလက်များကို Google နှင့် မျှဝေနိုင်သည်။\n\nသင်မျှဝေသည့် အချက်အလက်များကို ပြောင်းရန် ""ဆက်တင်များ > အက်ပ်များ > မူရင်းအက်ပ်များ > ဒစ်ဂျစ်တယ် Assistant အက်ပ်"" သို့ သွားပါ။" + "ဤဝန်ဆောင်မှုကို အသုံးပြုရန် assistant ရွေးပါ" + "ဖန်သားပြင်ပေါ်ရှိ စာသားကို နားထောင်ရန် (သို့) ဘာသာပြန်ဆိုရန် ‘ဆက်တင်များ’ တွင် ဒစ်ဂျစ်တယ် assistant အက်ပ် ရွေးပါ" + "ဤဝန်ဆောင်မှုကို သုံးရန် assistant ကို ပြောင်းပါ" + "ဖန်သားပြင်ပေါ်ရှိ စာသားကို နားထောင်ရန် (သို့) ဘာသာပြန်ဆိုရန် ‘ဆက်တင်များ’ တွင် ဒစ်ဂျစ်တယ် assistant အက်ပ်ကို ပြောင်းပါ" + "ဤဖန်သားပြင်ပေါ်ရှိ စာသားကို နားထောင်ရန် ဤနေရာကို တို့ပါ" + "ဤဖန်သားပြင်ပေါ်ရှိ စာသားကို ဘာသာပြန်ဆိုရန် ဤနေရာကို တို့ပါ" + "ဤအက်ပ်ကို မျှဝေ၍မရပါ" + diff --git a/go/quickstep/res/values-nb/strings.xml b/go/quickstep/res/values-nb/strings.xml new file mode 100644 index 0000000000..662b544fd3 --- /dev/null +++ b/go/quickstep/res/values-nb/strings.xml @@ -0,0 +1,20 @@ + + + "Del appen" + "Lytt" + "Oversett" + "Lens" + "GREIT" + "AVBRYT" + "INNSTILLINGER" + "Oversett eller lytt til tekst på skjermen" + "Informasjon som tekst på skjermen, nettadresser og skjermdumper kan deles med Google.\n\nFor å endre hvilken informasjon du deler, gå til ""Innstillinger > Apper > Standardapper > Digital assistent-app""." + "Velg en assistent for å bruke denne funksjonen" + "For å høre eller oversette tekst på skjermen, velg en digital assistent-app i innstillingene" + "Endre assistenten for å bruke denne funksjonen" + "For å høre eller oversette tekst på skjermen, endre digital assistent-appen i innstillingene" + "Trykk her for å høre teksten på denne skjermen" + "Trykk her for å oversette teksten på denne skjermen" + "Denne appen kan ikke deles" + diff --git a/go/quickstep/res/values-ne/strings.xml b/go/quickstep/res/values-ne/strings.xml new file mode 100644 index 0000000000..11a70dd74a --- /dev/null +++ b/go/quickstep/res/values-ne/strings.xml @@ -0,0 +1,20 @@ + + + "एप सेयर गर्नुहोस्" + "सुन्नुहोस्" + "अनुवाद गर्नुहोस्" + "लेन्स" + "बुझेँ" + "रद्द गर्नुहोस्" + "सेटिङ" + "स्क्रिनमा देखिने पाठ अनुवाद गरियोस् वा पढेर सुनाइयोस्" + "तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न ""सेटिङ > एप > डिफल्ट एप > डिजिटल सहायक एप"" मा जानुहोस्।" + "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने कुनै सहायक छनौट गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप छनौट गर्नुहोस्" + "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने आफ्नो सहायक परिवर्तन गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप परिर्वर्तन गर्नुहोस्" + "तपाईं यो स्क्रिनमा देखिने पाठ सुन्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" + "तपाईं यो स्क्रिनमा देखिने पाठ अनुवाद गर्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" + "यो एप अरूलाई चलाउन दिन मिल्दैन" + diff --git a/go/quickstep/res/values-nl/strings.xml b/go/quickstep/res/values-nl/strings.xml new file mode 100644 index 0000000000..2d4c6e260b --- /dev/null +++ b/go/quickstep/res/values-nl/strings.xml @@ -0,0 +1,20 @@ + + + "App delen" + "Luisteren" + "Vertalen" + "Lens" + "OK" + "ANNULEREN" + "INSTELLINGEN" + "Tekst op het scherm vertalen of beluisteren" + "Informatie zoals tekst op je scherm, webadressen en screenshots kan met Google worden gedeeld.\n\nAls je wilt aanpassen welke informatie je deelt, ga je naar ""Instellingen > Apps > Standaard-apps > Digitale-assistent-app""." + "Kies een assistent om deze functie te gebruiken" + "Als je tekst op je scherm wilt beluisteren of vertalen, kies je een digitale-assistent-app in Instellingen" + "Wijzig de assistent om deze functie te gebruiken" + "Als je tekst op je scherm wilt beluisteren of vertalen, wijzig je de digitale-assistent-app in Instellingen" + "Tik hier om tekst op dit scherm te beluisteren" + "Tik hier om tekst op dit scherm te vertalen" + "Deze app kan niet worden gedeeld" + diff --git a/go/quickstep/res/values-or/strings.xml b/go/quickstep/res/values-or/strings.xml index db328ece9e..6a3c5dc0c8 100644 --- a/go/quickstep/res/values-or/strings.xml +++ b/go/quickstep/res/values-or/strings.xml @@ -1,12 +1,12 @@ - "ଆପ୍ ସେୟାର୍ କରନ୍ତୁ" + "ଆପ ସେୟାର କରନ୍ତୁ" "ଶୁଣନ୍ତୁ" "ଅନୁବାଦ କରନ୍ତୁ" "Lens" "ବୁଝିଗଲି" - "ବାତିଲ୍ କରନ୍ତୁ" + "ବାତିଲ କରନ୍ତୁ" "ସେଟିଂସ" "ସ୍କିନରେ ଥିବା ଟେକ୍ସଟକୁ ଅନୁବାଦ କରନ୍ତୁ କିମ୍ବା ଶୁଣନ୍ତୁ" "ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଟେକ୍ସଟ, ୱେବ ଠିକଣା ଏବଂ ସ୍କ୍ରିନସଟଗୁଡ଼ିକ ପରି ସୂଚନାକୁ Google ସହ ସେୟାର କରାଯାଇପାରେ।\n\nଆପଣ କେଉଁ ସୂଚନାକୁ ସେୟାର କରନ୍ତି ତାହା ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ, ""ସେଟିଂସ > ଆପ୍ସ > ଡିଫଲ୍ଟ ଆପ୍ସ > Digital assistant ଆପ""କୁ ଯାଆନ୍ତୁ।" @@ -16,4 +16,5 @@ "ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଥିବା ଟେକ୍ସଟକୁ ଶୁଣିବା କିମ୍ବା ଅନୁବାଦ କରିବା ପାଇଁ, ସେଟିଂସରେ ଆପଣଙ୍କ Digital assistant ଆପକୁ ବଦଳାନ୍ତୁ" "ଏହି ସ୍କ୍ରିନରେ ଥିବା ଟେକ୍ସଟକୁ ଶୁଣିବା ପାଇଁ ଏଠାରେ ଟାପ୍ କରନ୍ତୁ" "ଏହି ସ୍କ୍ରିନରେ ଥିବା ଟେକ୍ସଟକୁ ଅନୁବାଦ କରିବା ପାଇଁ ଏଠାରେ ଟାପ୍ କରନ୍ତୁ" + "ଏହି ଆପ ସେୟାର କରାଯାଇପାରିବ ନାହିଁ" diff --git a/go/quickstep/res/values-pa/strings.xml b/go/quickstep/res/values-pa/strings.xml new file mode 100644 index 0000000000..8549b586c1 --- /dev/null +++ b/go/quickstep/res/values-pa/strings.xml @@ -0,0 +1,20 @@ + + + "ਐਪ ਨੂੰ ਸਾਂਝਾ ਕਰੋ" + "ਸੁਣੋ" + "ਅਨੁਵਾਦ ਕਰੋ" + "Lens" + "ਸਮਝ ਲਿਆ" + "ਰੱਦ ਕਰੋ" + "ਸੈਟਿੰਗਾਂ" + "ਸਕ੍ਰੀਨ \'ਤੇ ਦਿੱਤੀ ਲਿਖਤ ਦਾ ਅਨੁਵਾਦ ਕਰੋ ਜਾਂ ਉਸਨੂੰ ਸੁਣੋ" + "ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿੱਤੀ ਲਿਖਤ, ਵੈੱਬ ਪਤਿਆਂ ਅਤੇ ਸਕ੍ਰੀਨਸ਼ਾਟਾਂ ਵਰਗੀ ਜਾਣਕਾਰੀ ਨੂੰ Google ਨਾਲ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ।\n\nਤੁਹਾਡੇ ਵੱਲੋਂ ਸਾਂਝੀ ਕੀਤੀ ਜਾਣਕਾਰੀ ਨੂੰ ਬਦਲਣ ਲਈ, ""ਸੈਟਿੰਗਾਂ > ਐਪਾਂ > ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਐਪਾਂ > ਡਿਜੀਟਲ ਸਹਾਇਕ ਐਪ"" \'ਤੇ ਜਾਓ।" + "ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਨੂੰ ਵਰਤਣ ਲਈ ਸਹਾਇਕ ਐਪ ਚੁਣੋ" + "ਆਪਣੀ ਸਕ੍ਰੀਨ \'ਤੇ ਲਿਖਤ ਨੂੰ ਸੁਣਨ ਅਤੇ ਉਸਦਾ ਅਨੁਵਾਦ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਡਿਜੀਟਲ ਸਹਾਇਕ ਐਪ ਚੁਣੋ" + "ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਨੂੰ ਵਰਤਣ ਲਈ ਆਪਣੀ ਸਹਾਇਕ ਐਪ ਬਦਲੋ" + "ਆਪਣੀ ਸਕ੍ਰੀਨ \'ਤੇ ਲਿਖਤ ਨੂੰ ਸੁਣਨ ਅਤੇ ਉਸਦਾ ਅਨੁਵਾਦ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਆਪਣੀ ਡਿਜੀਟਲ ਸਹਾਇਕ ਐਪ ਬਦਲੋ" + "ਇਸ ਸਕ੍ਰੀਨ \'ਤੇ ਲਿਖਤ ਨੂੰ ਸੁਣਨ ਲਈ ਇੱਥੇ ਟੈਪ ਕਰੋ" + "ਇਸ ਸਕ੍ਰੀਨ \'ਤੇ ਲਿਖਤ ਦਾ ਅਨੁਵਾਦ ਕਰਨ ਲਈ ਇੱਥੇ ਟੈਪ ਕਰੋ" + "ਇਸ ਐਪ ਨੂੰ ਸਾਂਝਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ" + diff --git a/go/quickstep/res/values-pl/strings.xml b/go/quickstep/res/values-pl/strings.xml new file mode 100644 index 0000000000..5dc66f538b --- /dev/null +++ b/go/quickstep/res/values-pl/strings.xml @@ -0,0 +1,20 @@ + + + "Udostępnij aplikację" + "Posłuchaj" + "Przetłumacz" + "Obiektyw" + "OK" + "ANULUJ" + "USTAWIENIA" + "Przetłumacz lub odsłuchaj tekst na ekranie" + "Informacje takie jak tekst na ekranie, adresy internetowe i zrzuty ekranu mogą być udostępniane Google.\n\nAby zmienić zakres udostępnianych informacji, kliknij ""Ustawienia > Aplikacje > Aplikacje domyślne > Asystent cyfrowy""." + "Aby użyć tej funkcji, wybierz asystenta" + "Aby odsłuchać lub przetłumaczyć tekst widoczny na ekranie, wybierz w Ustawieniach aplikację asystenta cyfrowego" + "Aby użyć tej funkcji, zmień asystenta" + "Aby odsłuchać lub przetłumaczyć tekst widoczny na ekranie, zmień w Ustawieniach aplikację asystenta cyfrowego" + "Kliknij tutaj, aby odsłuchać tekst widoczny na ekranie" + "Kliknij tutaj, aby przetłumaczyć tekst widoczny na ekranie" + "Tej aplikacji nie można udostępnić" + diff --git a/go/quickstep/res/values-pt-rPT/strings.xml b/go/quickstep/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..e64f520ad3 --- /dev/null +++ b/go/quickstep/res/values-pt-rPT/strings.xml @@ -0,0 +1,20 @@ + + + "Partilhar app" + "Ouvir" + "Traduzir" + "Lens" + "OK" + "CANCELAR" + "DEFINIÇÕES" + "Traduza ou ouça o texto no ecrã" + "Informações como o texto no ecrã, endereços Web e capturas de ecrã podem ser partilhadas com a Google.\n\nPara alterar as informações que partilha, aceda a ""Definições > Apps > App predefinidas > App de assistente digital""." + "Escolha um assistente para usar esta funcionalidade" + "Para ouvir ou traduzir o texto no ecrã, escolha uma app de assistente digital nas Definições" + "Mude de assistente para usar esta funcionalidade" + "Para ouvir ou traduzir o texto no ecrã, mude de app de assistente digital nas Definições" + "Toque aqui para ouvir o texto neste ecrã" + "Toque aqui para traduzir o texto neste ecrã" + "Não é possível partilhar esta app" + diff --git a/go/quickstep/res/values-pt/strings.xml b/go/quickstep/res/values-pt/strings.xml new file mode 100644 index 0000000000..ba59231989 --- /dev/null +++ b/go/quickstep/res/values-pt/strings.xml @@ -0,0 +1,20 @@ + + + "Compartilhar app" + "Ouvir" + "Traduzir" + "Lens" + "OK" + "CANCELAR" + "CONFIGURAÇÕES" + "Traduzir ou ouvir o texto na tela" + "Informações como o texto exibido, endereços da Web e capturas de tela poderão ser compartilhadas com o Google.\n\nPara mudar quais dados são compartilhados, acesse ""Configurações > Apps > Apps padrão > App assistente digital""." + "Escolha um assistente para usar esse recurso" + "Para ouvir ou traduzir o texto exibido na tela, escolha um app assistente digital nas Configurações" + "Mude seu assistente para usar esse recurso" + "Para ouvir ou traduzir o texto exibido na tela, mude seu app assistente digital nas Configurações" + "Toque aqui para ouvir o texto exibido na tela" + "Toque aqui para traduzir o texto exibido na tela" + "Não é possível compartilhar o app" + diff --git a/go/quickstep/res/values-ro/strings.xml b/go/quickstep/res/values-ro/strings.xml new file mode 100644 index 0000000000..3d6f0d8dd6 --- /dev/null +++ b/go/quickstep/res/values-ro/strings.xml @@ -0,0 +1,20 @@ + + + "Trimite aplicația" + "Ascultă" + "Tradu" + "Lens" + "OK" + "ANULEAZĂ" + "SETĂRI" + "Tradu sau ascultă textul de pe ecran" + "Informații precum textul de pe ecran, adresele web și capturile de ecran pot fi trimise la Google.\n\nCa să schimbi informațiile trimise, accesează ""Setări > Aplicații > Aplicații prestabilite > Aplicația asistent digital""." + "Alege un asistent pentru a folosi această funcție" + "Pentru a asculta sau a traduce text de pe ecran, alege o aplicație asistent digital în Setări" + "Schimbă asistentul pentru a folosi această funcție" + "Pentru a asculta sau a traduce text de pe ecran, schimbă aplicația asistent digital în Setări" + "Atinge aici pentru a asculta text de pe ecran" + "Atinge aici pentru a traduce text de pe ecran" + "Aplicația nu poate fi distribuită" + diff --git a/go/quickstep/res/values-ru/strings.xml b/go/quickstep/res/values-ru/strings.xml new file mode 100644 index 0000000000..d845c354d2 --- /dev/null +++ b/go/quickstep/res/values-ru/strings.xml @@ -0,0 +1,20 @@ + + + "Поделиться" + "Слушать" + "Перевести" + "Объектив" + "ОК" + "ОТМЕНА" + "ОТКРЫТЬ НАСТРОЙКИ" + "Перевод или прослушивание текста на экране" + "Определенные сведения (например, текст на экране, веб-адреса и скриншоты) могут быть отправлены в Google.\n\nИзменить информацию, которой вы делитесь, можно в меню ""Настройки > Приложения > Приложения по умолчанию > Цифровой помощник""." + "Выберите помощника, чтобы пользоваться этой функцией" + "Чтобы слушать или переводить текст на экране, укажите цифрового помощника в настройках." + "Смените помощника, чтобы пользоваться этой функцией" + "Чтобы слушать или переводить текст на экране, выберите другого цифрового помощника в настройках." + "Нажмите, чтобы прослушать текст на этой странице" + "Нажмите, чтобы перевести текст на этой странице" + "Невозможно предоставить доступ к этому приложению." + diff --git a/go/quickstep/res/values-si/strings.xml b/go/quickstep/res/values-si/strings.xml new file mode 100644 index 0000000000..52718f2d8f --- /dev/null +++ b/go/quickstep/res/values-si/strings.xml @@ -0,0 +1,20 @@ + + + "යෙදුම බෙදා ගන්න" + "සවන් දෙන්න" + "පරිවර්තනය කරන්න" + "Lens" + "තේරුණා" + "අවලංගු කරන්න" + "සැකසීම්" + "තිරය මත පෙළ පරිවර්තනය කරන්න හෝ එයට සවන් දෙන්න" + "ඔබගේ තිරයේ ඇති පෙළ, වෙබ් ලිපින සහ තිර රූ වැනි තොරතුරු Google සමඟ බෙදා ගත හැකිය.\n\nඔබ බෙදා ගන්නා තොරතුරු වෙනස් කිරීමට, ""සැකසීම් > යෙදුම් > පෙරනිමි යෙදුම් > ඩිජිටල් සහායක යෙදුම"" වෙත යන්න." + "මෙම විශේෂාංගය භාවිත කිරීමට සහායකයකු තෝරන්න" + "ඔබගේ තිරයේ පෙළ සවන් දීමට හෝ පරිවර්තනය කිරීමට, සැකසීම්වල ඩිජිටල් සහායක යෙදුමක් තෝරන්න" + "මෙම විශේෂාංගය භාවිත කිරීමට ඔබගේ සහායක වෙනස් කරන්න" + "ඔබගේ තිරයේ පෙළට සවන් දීමට හෝ පරිවර්තනය කිරීමට, සැකසීම් තුළ ඔබගේ ඩිජිටල් සහායක යෙදුම වෙනස් කරන්න" + "මෙම තිරයේ පෙළට සවන් දීමට මෙහි තට්ටු කරන්න" + "මෙම තිරයේ පෙළ පරිවර්තනය කිරීමට මෙහි තට්ටු කරන්න" + "මෙම යෙදුම බෙදා ගත නොහැකිය" + diff --git a/go/quickstep/res/values-sk/strings.xml b/go/quickstep/res/values-sk/strings.xml new file mode 100644 index 0000000000..89291af0f5 --- /dev/null +++ b/go/quickstep/res/values-sk/strings.xml @@ -0,0 +1,20 @@ + + + "Zdieľať aplikáciu" + "Počúvať" + "Preložiť" + "Lens" + "DOBRE" + "ZRUŠIŤ" + "NASTAVENIA" + "Prekladajte alebo počúvajte text na obrazovke" + "Údaje, napríklad text na obrazovke, webové adresy a snímky obrazovky, môžu byť zdieľané s Googlom.\n\nAk chcete zmeniť, ktoré údaje zdieľate, prejdite do sekcie ""Nastavenia > Aplikácie > Predvolené aplikácie > Digitálny asistent""." + "Ak chcete používať túto funkciu, vyberte asistenta" + "Ak si chcete vypočuť alebo nechať preložiť text na obrazovke, vyberte v Nastaveniach aplikáciu digitálneho asistenta" + "Ak chcete použígvať túto funkciu, zmeňte svojho asistenta" + "Ak si chcete vypočuť alebo nechať preložiť text na obrazovke, zmeňte v Nastaveniach aplikáciu digitálneho asistenta" + "Klepnutím tu si vypočujte text na tejto obrazovke" + "Klepnutím tu si nechajte preložiť text na tejto obrazovke" + "Táto aplikácia sa nedá zdieľať" + diff --git a/go/quickstep/res/values-sl/strings.xml b/go/quickstep/res/values-sl/strings.xml new file mode 100644 index 0000000000..afa674e058 --- /dev/null +++ b/go/quickstep/res/values-sl/strings.xml @@ -0,0 +1,20 @@ + + + "Deli aplikacijo" + "Poslušanje" + "Prevedi" + "Lens" + "RAZUMEM" + "PREKLIČI" + "NASTAVITVE" + "Prevod ali poslušanje besedila na zaslonu" + "Podatki, kot so besedilo na zaslonu, spletni naslovi in posnetki zaslonov, bodo morda deljeni z Googlom.\n\nČe želite spremeniti, katere podatke delite, odprite ""Nastavitve > Aplikacije > Privzete aplikacije > Digitalni pomočnik""." + "Za uporabo te funkcije izberite pomočnika" + "Za poslušanje ali prevod besedila na zaslonu v nastavitvah izberite digitalnega pomočnika." + "Za uporabo te funkcije izberite drugega pomočnika" + "Za poslušanje ali prevod besedila na zaslonu v nastavitvah izberite drugega digitalnega pomočnika." + "Dotaknite se tukaj za poslušanje besedila na zaslonu." + "Dotaknite se tukaj za prevod besedila na zaslonu." + "Te aplikacije ni mogoče deliti z drugimi." + diff --git a/go/quickstep/res/values-sq/strings.xml b/go/quickstep/res/values-sq/strings.xml new file mode 100644 index 0000000000..123ca39669 --- /dev/null +++ b/go/quickstep/res/values-sq/strings.xml @@ -0,0 +1,20 @@ + + + "Ndaj aplikacionin" + "Dëgjo" + "Përkthe" + "Lentja" + "E KUPTOVA" + "ANULO" + "CILËSIMET" + "Përkthe ose dëgjo tekstin në ekran" + "Informacionet si teksti në ekranin tënd, adresat e uebit dhe pamjet e ekranit mund të ndahen me Google.\n\nPër të ndryshuar se çfarë informacionesh ndahen, shko te ""Cilësimet > Aplikacionet > Aplikacionet e parazgjedhura > Aplikacioni i asistentit dixhital""." + "Zgjidh një asistent për të përdorur këtë veçori" + "Për të dëgjuar ose përkthyer tekstin në ekran, zgjidh një aplikacion të asistentit dixhital te \"Cilësimet\"" + "Ndrysho asistentin për të përdorur këtë veçori" + "Për të dëgjuar ose përkthyer tekstin në ekran, ndrysho aplikacionin e asistentit dixhital te \"Cilësimet\"" + "Trokit këtu për të dëgjuar tekstin në këtë ekran" + "Trokit këtu për të përkthyer tekstin në këtë ekran" + "Ky aplikacion nuk mund të ndahet" + diff --git a/go/quickstep/res/values-sr/strings.xml b/go/quickstep/res/values-sr/strings.xml new file mode 100644 index 0000000000..749fc0ece3 --- /dev/null +++ b/go/quickstep/res/values-sr/strings.xml @@ -0,0 +1,20 @@ + + + "Дели апликацију" + "Пусти" + "Преведи" + "Објектив" + "ВАЖИ" + "ОТКАЖИ" + "ПОДЕШАВАЊА" + "Преводите или слушајте текст на екрану" + "Информације попут текста на екрану, веб-адреса и снимака екрана могу да се деле са Google-ом.\n\nДа бисте променили информације које делите, идите у ""Подешавања > Апликације > Подразумеване апликације > Апликација дигиталног помоћника""." + "Одаберите помоћника да бисте користили ову функцију" + "Да бисте чули текст са екрана или га превели, одаберите апликацију дигиталног помоћника у Подешавањима" + "Промените помоћника да бисте користили ову функцију" + "Да бисте чули текст са екрана или га превели, промените апликацију дигиталног помоћника у Подешавањима" + "Додирните овде да бисте чули текст са овог екрана" + "Додирните овде да бисте превели текст са овог екрана" + "Ова апликација не може да се дели" + diff --git a/go/quickstep/res/values-sv/strings.xml b/go/quickstep/res/values-sv/strings.xml new file mode 100644 index 0000000000..e76fe7f7c9 --- /dev/null +++ b/go/quickstep/res/values-sv/strings.xml @@ -0,0 +1,20 @@ + + + "Dela app" + "Lyssna" + "Översätt" + "Lens" + "OK" + "AVBRYT" + "INSTÄLLNINGAR" + "Översätt eller lyssna på texten på skärmen" + "Information som text på skärmen, webbadresser och skärmbilder kan delas med Google.\n\nFör att ändra vilken information du delar öppnar du ""Inställningar > Appar > Standardappar > Digital assistentapp""." + "Välj en assistent om du vill använda den här funktionen" + "Välj en digital assistentapp i Inställningar om du vill lyssna på eller översätta text på skärmen" + "Byt assistent om du vill använda den här funktionen" + "Byt digital assistentapp i Inställningar om du vill lyssna på eller översätta text på skärmen" + "Tryck här för att lyssna på texten på skärmen" + "Tryck här för att översätta texten på skärmen" + "Den här appen kan inte delas" + diff --git a/go/quickstep/res/values-sw/strings.xml b/go/quickstep/res/values-sw/strings.xml new file mode 100644 index 0000000000..5a25702192 --- /dev/null +++ b/go/quickstep/res/values-sw/strings.xml @@ -0,0 +1,20 @@ + + + "Shiriki Programu" + "Sikiliza" + "Tafsiri" + "Lenzi" + "NIMEELEWA" + "GHAIRI" + "MIPANGILIO" + "Tafsiri au usikilize maandishi kwenye skrini" + "Maelezo kama vile maandishi kwenye skrini yako, anwani za wavuti na picha za skrini yanaweza kushirikiwa na Google.\n\nIli ubadilishe maelezo unayoshiriki, nenda kwenye ""Mipangilio > Programu > Programu chaguomsingi > Programu ya mratibu dijitali""." + "Chagua programu ya mratibu ili utumie kipengele hiki" + "Ili usikilize au utafsiri maandishi kwenye skrini yako, chagua programu ya mratibu dijitali katika Mipangilio" + "Badilisha programu yako ya mratibu ili utumie kipengele hiki" + "Ili usikilize au utafsiri maandishi kwenye skrini yako, badilisha programu yako ya mratibu dijitali katika Mipangilio" + "Gusa hapa ili usikilize maandishi kwenye skrini hii" + "Gusa hapa ili utafsiri maandishi kwenye skrini hii" + "Huwezi kushiriki programu hii" + diff --git a/go/quickstep/res/values-ta/strings.xml b/go/quickstep/res/values-ta/strings.xml index 89848d5213..cd97aa3c7a 100644 --- a/go/quickstep/res/values-ta/strings.xml +++ b/go/quickstep/res/values-ta/strings.xml @@ -16,4 +16,5 @@ "திரையில் தோன்றும் வார்த்தைகளைக் கேட்கவோ மொழிபெயர்க்கவோ அமைப்புகளில் டிஜிட்டல் அசிஸ்டண்ட் ஆப்ஸை மாற்றவும்" "இந்தத் திரையில் தோன்றும் வார்த்தைகளைக் கேட்க இங்கே தட்டவும்" "இந்தத் திரையில் தோன்றும் வார்த்தைகளை மொழிபெயர்க்க இங்கே தட்டவும்" + "இந்த ஆப்ஸைப் பகிர முடியாது" diff --git a/go/quickstep/res/values-te/strings.xml b/go/quickstep/res/values-te/strings.xml new file mode 100644 index 0000000000..656adf687b --- /dev/null +++ b/go/quickstep/res/values-te/strings.xml @@ -0,0 +1,20 @@ + + + "యాప్‌ను షేర్ చేయండి" + "వినండి" + "అనువదించండి" + "Lens" + "అర్థమైంది" + "రద్దు చేయండి" + "సెట్టింగ్‌లు" + "స్క్రీన్‌పై టెక్స్ట్ అనువదించండి లేదా వినండి" + "మీ స్క్రీన్‌పై టెక్స్ట్, వెబ్ అడ్రస్‌లు, అలాగే స్క్రీన్‌షాట్‌ల వంటి సమాచారం Googleతో షేర్ చేయడం జరగవచ్చు.\n\nమీరు ఏ సమాచారాన్ని షేర్ చేసుకుంటారో మార్చడానికి ""సెట్టింగ్‌లు > యాప్‌లు > ఆటోమేటిక్ సెట్టింగ్ యాప్‌లు > డిజిటల్ అసిస్టెంట్ యాప్‌ల""‌కు వెళ్లండి." + "ఈ ఫీచర్‌ను ఉపయోగించడానికి అసిస్టెంట్‌ను ఎంచుకోండి" + "మీ స్క్రీన్‌పై ఉన్న టెక్స్ట్‌ను వినడానికి లేదా అనువదించడానికి, సెట్టింగ్‌లలో డిజిటల్ అసిస్టెంట్ యాప్‌ను ఎంచుకోండి" + "ఈ ఫీచర్‌ను ఉపయోగించడానికి మీ అసిస్టెంట్‌ను మార్చండి" + "మీ స్క్రీన్‌పై ఉన్న టెక్స్ట్‌ను వినడానికి లేదా అనువదించడానికి, సెట్టింగ్‌లలో మీ డిజిటల్ అసిస్టెంట్ యాప్‌ను మార్చండి" + "ఈ స్క్రీన్‌పై ఉన్న టెక్స్ట్‌ను వినడానికి ఇక్కడ ట్యాప్ చేయండి" + "ఈ స్క్రీన్‌పై ఉన్న టెక్స్ట్‌ను అనువదించడానికి ఇక్కడ ట్యాప్ చేయండి" + "ఈ యాప్‌ను షేర్ చేయడం సాధ్యపడదు" + diff --git a/go/quickstep/res/values-th/strings.xml b/go/quickstep/res/values-th/strings.xml new file mode 100644 index 0000000000..8ce2ffc764 --- /dev/null +++ b/go/quickstep/res/values-th/strings.xml @@ -0,0 +1,20 @@ + + + "แชร์แอป" + "ฟัง" + "แปลภาษา" + "Lens" + "รับทราบ" + "ยกเลิก" + "การตั้งค่า" + "แปลหรือฟังข้อความบนหน้าจอ" + "อาจมีการแชร์ข้อมูลอย่างเช่น ข้อความบนหน้าจอ ที่อยู่เว็บ และภาพหน้าจอกับ Google\n\nหากต้องการเปลี่ยนประเภทข้อมูลที่คุณแชร์ ให้ไปที่""การตั้งค่า > แอป > แอปเริ่มต้น > แอปผู้ช่วยดิจิทัล" + "เลือกผู้ช่วยเพื่อใช้ฟีเจอร์นี้" + "เลือกแอปผู้ช่วยดิจิทัลในการตั้งค่าเพื่อฟังหรือแปลข้อความบนหน้าจอ" + "เปลี่ยนผู้ช่วยเพื่อใช้ฟีเจอร์นี้" + "เปลี่ยนแอปผู้ช่วยดิจิทัลในการตั้งค่าเพื่อฟังหรือแปลข้อความบนหน้าจอ" + "แตะที่นี่เพื่อฟังข้อความบนหน้าจอนี้" + "แตะที่นี่เพื่อแปลข้อความบนหน้าจอนี้" + "แชร์แอปนี้ไม่ได้" + diff --git a/go/quickstep/res/values-tl/strings.xml b/go/quickstep/res/values-tl/strings.xml new file mode 100644 index 0000000000..2da5f59a40 --- /dev/null +++ b/go/quickstep/res/values-tl/strings.xml @@ -0,0 +1,20 @@ + + + "Ibahagi ang App" + "Makinig" + "Isalin" + "Lens" + "OK" + "KANSELAHIN" + "MGA SETTING" + "I-translate o pakinggan ang text sa screen" + "Posibleng ibahagi sa Google ang impormasyong gaya ng text sa iyong screen, mga web address, at screenshot.\n\nPara baguhin kung anong impormasyon ang ibinabahagi mo, pumunta sa ""Mga Setting > Mga App > Mga default na app > Digital assistant app""." + "Pumili ng assistant para magamit ang feature na ito" + "Para pakinggan o isalin ang text sa iyong screen, pumili ng app ng digital na assistant sa Mga Setting" + "Palitan ang iyong assistant para magamit ang feature na ito" + "Para pakinggan o isalin ang text sa iyong screen, palitan ang iyong app ng digital na assistant sa Mga Setting" + "Mag-tap dito para pakinggan ang text sa screen na ito" + "Mag-tap dito para isalin ang text sa screen na ito" + "Hindi maibabahagi ang app na ito" + diff --git a/go/quickstep/res/values-tr/strings.xml b/go/quickstep/res/values-tr/strings.xml new file mode 100644 index 0000000000..dd2e9077b9 --- /dev/null +++ b/go/quickstep/res/values-tr/strings.xml @@ -0,0 +1,20 @@ + + + "Uygulamayı paylaş" + "Dinle" + "Çevir" + "Lens" + "ANLADIM" + "İPTAL" + "AYARLAR" + "Ekrandaki metni çevirin veya dinleyin" + "Ekranınızdaki metin, web adresleri ve ekran görüntüleri gibi bilgiler Google ile paylaşılabilir.\n\nPaylaştığınız bilgileri değiştirmek için ""Ayarlar > Uygulamalar > Varsayılan uygulamalar > Dijital asistan uygulamasına gidin""." + "Bu özelliği kullanmak için bir asistan seçin" + "Ekranınızdaki metni dinlemek veya çevirmek için Ayarlar\'dan bir dijital asistan uygulaması seçin" + "Bu özelliği kullanmak için asistanınızı değiştirin" + "Ekranınızdaki metni dinlemek veya çevirmek için Ayarlar\'dan dijital asistan uygulamanızı değiştirin" + "Bu ekrandaki metni dinlemek için buraya dokunun" + "Bu ekrandaki metni çevirmek için buraya dokunun" + "Bu uygulama paylaşılamaz" + diff --git a/go/quickstep/res/values-uk/strings.xml b/go/quickstep/res/values-uk/strings.xml new file mode 100644 index 0000000000..c59b3bf059 --- /dev/null +++ b/go/quickstep/res/values-uk/strings.xml @@ -0,0 +1,20 @@ + + + "Поділитися додатком" + "Слухати" + "Перекласти" + "Об’єктив" + "OK" + "СКАСУВАТИ" + "НАЛАШТУВАННЯ" + "Перекласти або прослухати текст на екрані" + "Певна інформація (наприклад, текст на екрані, веб-адреси, знімки екрана) може надсилатися на сервери Google.\n\nЩоб змінити типи даних, які надсилаються, відкрийте ""Налаштування > Додатки > Додатки за умовчанням > Цифровий помічник""." + "Щоб користуватися цією функцією, виберіть помічника" + "Щоб прослухати чи перекласти текст на екрані, виберіть цифрового помічника в налаштуваннях" + "Змініть помічника, щоб користуватися цією функцією" + "Щоб прослухати чи перекласти текст на екрані, змініть цифрового помічника в налаштуваннях" + "Натисніть тут, щоб прослухати текст на цьому екрані" + "Натисніть тут, щоб перекласти текст на цьому екрані" + "Цим додатком не можна поділитися" + diff --git a/go/quickstep/res/values-ur/strings.xml b/go/quickstep/res/values-ur/strings.xml new file mode 100644 index 0000000000..dd6c2cccad --- /dev/null +++ b/go/quickstep/res/values-ur/strings.xml @@ -0,0 +1,20 @@ + + + "ایپ کا اشتراک کریں" + "سنیں" + "‏Google ترجمہ" + "‏Google لینز" + "سمجھ آ گئی" + "منسوخ کریں" + "ترتیبات" + "اسکرین پر موجود ٹیکسٹ کو سنیں یا ترجمہ کریں" + "‏آپ کی اسکرین پر ٹیکسٹ، ویب پتے اور اسکرین شاٹس جیسی معلومات کا اشتراک Google کے ساتھ کیا جا سکتا ہے۔\n\nآپ جس معلومات کا اشتراک کرتے ہیں اسے تبدیل کرنے کیلئے ""ترتیبات ‎>‎ ایپس ‎>‎ ڈیفالٹ ایپس ‎>‎ ڈیجیٹل اسسٹنٹ ایپ"" پر جائیں۔" + "اس خصوصیت کا استعمال کرنے کے لیے اسسٹنٹ کا انتخاب کریں" + "اپنی اسکرین پر موجود ٹیکسٹ کو سننے یا اس کا ترجمہ کرنے کیلئے ترتیبات میں ڈیجیٹل اسسٹنٹ ایپ کا انتخاب کریں" + "اس خصوصیت کا استعمال کرنے کے لیے اپنی اسسٹنٹ کا استعمال کریں" + "اپنی اسکرین پر موجود ٹیکسٹ کو سننے یا اس کا ترجمہ کرنے کیلئے ترتیبات میں اپنی ڈیجیٹل اسسٹنٹ ایپ کو تبدیل کریں" + "اس اسکرین پر موجود ٹیکسٹ کو سننے کے لیے یہاں تھپتھپائیں" + "اس اسکرین پر موجود ٹیکسٹ کا ترجمہ کرنے کے لیے یہاں تھپتھپائیں" + "اس ایپ کا اشتراک نہیں کیا جا سکتا" + diff --git a/go/quickstep/res/values-uz/strings.xml b/go/quickstep/res/values-uz/strings.xml new file mode 100644 index 0000000000..cb6b9e0c49 --- /dev/null +++ b/go/quickstep/res/values-uz/strings.xml @@ -0,0 +1,20 @@ + + + "Ilovani ulashish" + "Tinglash" + "Tarjima" + "Lens" + "OK" + "BEKOR QILISH" + "SOZLAMALAR" + "Ekrandagi matnni tarjima qilish yoki tinglash" + "Ekraningizdagi matn, veb-manzillar va skrinshot kabilar Googlega yuborilishi mumkin.\n\nQanday maʼlumotlarni ulashishni tanlash uchun ""Sozlamalar > Ilovalar > Standart ilovalar > Raqamli assistent"" ilovasini oching." + "Bu funksiyadan foydalanish uchun assistentni tanlang" + "Ekrandagi matnni eshittirish yoki tarjima qilish uchun Sozlamalar orqali raqamli assistent ilovasini tanlang" + "Bu funksiyadan foydalanish uchun assistentni almashtiring" + "Ekrandagi matnni eshittirish yoki tarjima qilish uchun Sozlamalar orqali raqamli assistent ilovasini almashtiring" + "Ekrandagi matnni eshittirish uchun bosing" + "Ekrandagi matnni tarjima qilish uchun bosing" + "Bu ilova ulashilmaydi" + diff --git a/go/quickstep/res/values-vi/strings.xml b/go/quickstep/res/values-vi/strings.xml new file mode 100644 index 0000000000..0bca1688b9 --- /dev/null +++ b/go/quickstep/res/values-vi/strings.xml @@ -0,0 +1,20 @@ + + + "Chia sẻ ứng dụng" + "Nghe" + "Dịch" + "Ống kính" + "OK" + "HỦY" + "CÀI ĐẶT" + "Dịch hoặc nghe văn bản trên màn hình" + "Những thông tin như văn bản trên màn hình, địa chỉ web và ảnh chụp màn hình có thể được chia sẻ với Google.\n\nĐể thay đổi những thông tin mà bạn chia sẻ, hãy chuyển đến phần ""Cài đặt > Ứng dụng > Ứng dụng mặc định > Ứng dụng trợ lý kỹ thuật số""." + "Chọn một ứng dụng trợ lý để dùng tính năng này" + "Để nghe hoặc dịch văn bản trên màn hình, hãy chọn một ứng dụng trợ lý kỹ thuật số trong phần Cài đặt" + "Thay đổi ứng dụng trợ lý để dùng tính năng này" + "Để nghe hoặc dịch văn bản trên màn hình, hãy thay đổi ứng dụng trợ lý kỹ thuật số trong phần Cài đặt" + "Nhấn vào đây để nghe văn bản trên màn hình này" + "Nhấn vào đây để dịch văn bản trên màn hình này" + "Bạn không thể chia sẻ ứng dụng này" + diff --git a/go/quickstep/res/values-zh-rCN/strings.xml b/go/quickstep/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..27bde298f6 --- /dev/null +++ b/go/quickstep/res/values-zh-rCN/strings.xml @@ -0,0 +1,20 @@ + + + "分享应用" + "聆听" + "翻译" + "智能镜头" + "知道了" + "取消" + "设置" + "翻译或聆听屏幕上的文字" + "系统可能会将屏幕上的文字、网址和屏幕截图等信息分享给 Google。\n\n如需更改要分享哪些信息,请依次转到""设置 > 应用 > 默认应用 > 数字助理应用""。" + "需选择一款助理应用,才能使用此功能" + "如需收听或翻译屏幕上的文字,请在“设置”部分选择一款数字助理应用" + "需更改助理应用,才能使用此功能" + "如需收听或翻译屏幕上的文字,请在“设置”部分更改数字助理应用" + "点按此处即可收听屏幕上的文字" + "点按此处即可翻译屏幕上的文字" + "无法分享此应用" + diff --git a/go/quickstep/res/values-zh-rHK/strings.xml b/go/quickstep/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..07cc125279 --- /dev/null +++ b/go/quickstep/res/values-zh-rHK/strings.xml @@ -0,0 +1,20 @@ + + + "分享應用程式" + "聆聽" + "翻譯" + "智能鏡頭" + "知道了" + "取消" + "設定" + "翻譯或聆聽畫面上的文字" + "可能會與 Google 分享螢幕上的文字、網址和螢幕截圖等資料。\n\n如要變更分享的資料,請前往 ""[設定] > [應用程式] > [預設應用程式] > [數碼助理應用程式]""。" + "必須選擇數碼助理才可使用此功能" + "如要聆聽或翻譯畫面上的文字,請在「設定」中選擇數碼助理應用程式" + "必須變更數碼助理才可使用此功能" + "如要聆聽或翻譯畫面上的文字,請在「設定」中變更數碼助理應用程式" + "輕按這裡即可聆聽此畫面上的文字" + "輕按這裡即可翻譯此畫面上的文字" + "無法分享此應用程式" + diff --git a/go/quickstep/res/values-zh-rTW/strings.xml b/go/quickstep/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..41fd8b1244 --- /dev/null +++ b/go/quickstep/res/values-zh-rTW/strings.xml @@ -0,0 +1,20 @@ + + + "分享應用程式" + "聆聽" + "翻譯" + "智慧鏡頭" + "我知道了" + "取消" + "設定" + "翻譯或朗讀畫面上的文字" + "系統可能會將畫面上的文字、網址和螢幕截圖等資訊分享給 Google。\n\n如要變更分享的資訊類型,請前往 [設定] > [應用程式] > [預設應用程式] > [數位助理應用程式]""。" + "必須選擇數位助理應用程式才能使用這項功能" + "如要聽取或翻譯畫面上的文字,請前往「設定」選擇數位助理應用程式" + "必須變更數位助理應用程式才能使用這項功能" + "如要聽取或翻譯畫面上的文字,請前往「設定」變更數位助理應用程式" + "輕觸這裡即可聽取這個畫面上的文字" + "輕觸這裡即可翻譯這個畫面上的文字" + "無法分享這個應用程式" + diff --git a/go/quickstep/res/values-zu/strings.xml b/go/quickstep/res/values-zu/strings.xml index 20b7894a1f..1be7898a4b 100644 --- a/go/quickstep/res/values-zu/strings.xml +++ b/go/quickstep/res/values-zu/strings.xml @@ -16,4 +16,5 @@ "Ukuze ulalele noma uhumushe umbhalo kusikrini sakho, shintsha i-app yomsizi wakho odijithali kokuthi Amasethingi" "Thepha lapha ukuze ulalele umbhalo kusikrini" "Thepha lapha ukuze uhumushe umbhalo kulesi sikrini" + "Le app ayikwazi ukwabiwa" diff --git a/go/quickstep/res/values/integers.xml b/go/quickstep/res/values/integers.xml new file mode 100644 index 0000000000..e6e8111eab --- /dev/null +++ b/go/quickstep/res/values/integers.xml @@ -0,0 +1,20 @@ + + + + + 200 + \ No newline at end of file diff --git a/go/quickstep/res/values/strings.xml b/go/quickstep/res/values/strings.xml index 8429f6a7f2..42f4702141 100644 --- a/go/quickstep/res/values/strings.xml +++ b/go/quickstep/res/values/strings.xml @@ -41,4 +41,8 @@ Tap here to listen to text on this screen Tap here to translate text on this screen + + + + This app can’t be shared diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml index 442c41375d..c659331bde 100644 --- a/go/quickstep/res/values/styles.xml +++ b/go/quickstep/res/values/styles.xml @@ -73,7 +73,7 @@ + + + + + + @@ -77,8 +97,8 @@ + + + + + + + + @@ -171,4 +219,28 @@ 24sp 2 - \ No newline at end of file + + + + + + + + + diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java deleted file mode 100644 index 7b2a0106f5..0000000000 --- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3; - -import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; -import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON; -import static com.android.launcher3.LauncherState.FLAG_HIDE_BACK_BUTTON; -import static com.android.launcher3.LauncherState.NORMAL; -import static com.android.launcher3.LauncherState.NO_OFFSET; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; -import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; -import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; -import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS; -import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; - -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.app.ActivityOptions; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender; -import android.graphics.Insets; -import android.hardware.SensorManager; -import android.hardware.devicestate.DeviceStateManager; -import android.os.Bundle; -import android.os.CancellationSignal; -import android.os.IBinder; -import android.view.View; -import android.view.WindowInsets; -import android.window.SplashScreen; - -import androidx.annotation.Nullable; - -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.dragndrop.DragOptions; -import com.android.launcher3.model.WellbeingModel; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.proxy.ProxyActivityStarter; -import com.android.launcher3.proxy.StartActivityParams; -import com.android.launcher3.statehandlers.BackButtonAlphaHandler; -import com.android.launcher3.statehandlers.DepthController; -import com.android.launcher3.statemanager.StateManager.StateHandler; -import com.android.launcher3.taskbar.LauncherTaskbarUIController; -import com.android.launcher3.taskbar.TaskbarManager; -import com.android.launcher3.uioverrides.RecentsViewStateController; -import com.android.launcher3.util.ActivityOptionsWrapper; -import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.IntSet; -import com.android.launcher3.util.ObjectWrapper; -import com.android.launcher3.util.UiThreadHelper; -import com.android.quickstep.OverviewCommandHelper; -import com.android.quickstep.RecentsModel; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; -import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener; -import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.TaskUtils; -import com.android.quickstep.TouchInteractionService.TISBinder; -import com.android.quickstep.util.LauncherUnfoldAnimationController; -import com.android.quickstep.util.ProxyScreenStatusProvider; -import com.android.quickstep.util.RemoteAnimationProvider; -import com.android.quickstep.util.RemoteFadeOutAnimationListener; -import com.android.quickstep.util.SplitSelectStateController; -import com.android.quickstep.util.TISBindHelper; -import com.android.quickstep.views.OverviewActionsView; -import com.android.quickstep.views.RecentsView; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.ActivityOptionsCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.unfold.UnfoldTransitionFactory; -import com.android.systemui.unfold.UnfoldTransitionProgressProvider; -import com.android.systemui.unfold.config.UnfoldTransitionConfig; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.List; -import java.util.stream.Stream; - -import app.lawnchair.LawnchairApp; - -/** - * Extension of Launcher activity to provide quickstep specific functionality - */ -public abstract class BaseQuickstepLauncher extends Launcher - implements NavigationModeChangeListener { - - private DepthController mDepthController = new DepthController(this); - private QuickstepTransitionManager mAppTransitionManager; - - /** - * Reusable command for applying the back button alpha on the background thread. - */ - public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA = - (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setNavBarButtonAlpha( - Float.intBitsToFloat(arg1), arg2 != 0); - - private OverviewActionsView mActionsView; - - private TISBindHelper mTISBindHelper; - private @Nullable TaskbarManager mTaskbarManager; - private @Nullable OverviewCommandHelper mOverviewCommandHelper; - private @Nullable LauncherTaskbarUIController mTaskbarUIController; - - // Will be updated when dragging from taskbar. - private @Nullable DragOptions mNextWorkspaceDragOptions = null; - - private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider; - private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this); - addMultiWindowModeChangedListener(mDepthController); - initUnfoldTransitionProgressProvider(); - } - - @Override - protected void onResume() { - super.onResume(); - - if (mLauncherUnfoldAnimationController != null) { - mLauncherUnfoldAnimationController.onResume(); - } - } - - @Override - protected void onPause() { - if (mLauncherUnfoldAnimationController != null) { - mLauncherUnfoldAnimationController.onPause(); - } - - super.onPause(); - } - - @Override - public void onDestroy() { - mAppTransitionManager.onActivityDestroyed(); - if (mUnfoldTransitionProgressProvider != null) { - mUnfoldTransitionProgressProvider.destroy(); - } - - SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this); - - mTISBindHelper.onDestroy(); - if (mTaskbarManager != null) { - mTaskbarManager.clearActivity(this); - } - - if (mLauncherUnfoldAnimationController != null) { - mLauncherUnfoldAnimationController.onDestroy(); - } - - super.onDestroy(); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - if (mOverviewCommandHelper != null) { - mOverviewCommandHelper.clearPendingCommands(); - } - } - - public QuickstepTransitionManager getAppTransitionManager() { - return mAppTransitionManager; - } - - @Override - public void onNavigationModeChanged(Mode newMode) { - getDragLayer().recreateControllers(); - if (mActionsView != null) { - mActionsView.updateVerticalMargin(newMode); - } - } - - @Override - public void onEnterAnimationComplete() { - super.onEnterAnimationComplete(); - // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled - // as a part of quickstep, so that high-res thumbnails can load the next time we enter - // overview - RecentsModel.INSTANCE.get(this).getThumbnailCache() - .getHighResLoadingState().setVisible(true); - } - - @Override - protected void handleGestureContract(Intent intent) { - if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) { - super.handleGestureContract(intent); - } - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - RecentsModel.INSTANCE.get(this).onTrimMemory(level); - } - - @Override - public void onUiChangedWhileSleeping() { - // Remove the snapshot because the content view may have obvious changes. - UI_HELPER_EXECUTOR.execute( - () -> ActivityManagerWrapper.getInstance().invalidateHomeTaskSnapshot(this)); - } - - @Override - protected void onScreenOff() { - super.onScreenOff(); - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - RecentsView recentsView = getOverviewPanel(); - recentsView.finishRecentsAnimation(true /* toRecents */, null); - } - } - - /** - * {@code LauncherOverlayCallbacks} scroll amount. - * Indicates transition progress to -1 screen. - * @param progress From 0 to 1. - */ - @Override - public void onScrollChanged(float progress) { - super.onScrollChanged(progress); - mDepthController.onOverlayScrollChanged(progress); - } - - @Override - public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { - if (requestCode != -1) { - mPendingActivityRequestCode = requestCode; - StartActivityParams params = new StartActivityParams(this, requestCode); - params.intentSender = intent; - params.fillInIntent = fillInIntent; - params.flagsMask = flagsMask; - params.flagsValues = flagsValues; - params.extraFlags = extraFlags; - params.options = options; - startActivity(ProxyActivityStarter.getLaunchIntent(this, params)); - } else { - super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, - flagsValues, extraFlags, options); - } - } - - @Override - public void startActivityForResult(Intent intent, int requestCode, Bundle options) { - if (requestCode != -1) { - mPendingActivityRequestCode = requestCode; - StartActivityParams params = new StartActivityParams(this, requestCode); - params.intent = intent; - params.options = options; - startActivity(ProxyActivityStarter.getLaunchIntent(this, params)); - } else { - super.startActivityForResult(intent, requestCode, options); - } - } - - @Override - protected void onDeferredResumed() { - super.onDeferredResumed(); - handlePendingActivityRequest(); - } - - @Override - public void onStateSetEnd(LauncherState state) { - super.onStateSetEnd(state); - handlePendingActivityRequest(); - } - - private void handlePendingActivityRequest() { - if (mPendingActivityRequestCode != -1 && isInState(NORMAL) - && ((getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) { - // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher. - onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null); - // ProxyActivityStarter is started with clear task to reset the task after which it - // removes the task itself. - startActivity(ProxyActivityStarter.getLaunchIntent(this, null)); - } - } - - @Override - protected void setupViews() { - super.setupViews(); - - SysUINavigationMode.INSTANCE.get(this).updateMode(); - mActionsView = findViewById(R.id.overview_actions_view); - RecentsView overviewPanel = (RecentsView) getOverviewPanel(); - SplitSelectStateController controller = - new SplitSelectStateController(mHandler, SystemUiProxy.INSTANCE.get(this), - getStateManager(), getDepthController()); - overviewPanel.init(mActionsView, controller); - mActionsView.setDp(getDeviceProfile()); - mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this)); - - mAppTransitionManager = new QuickstepTransitionManager(this); - mAppTransitionManager.registerRemoteAnimations(); - mAppTransitionManager.registerRemoteTransitions(); - - mTISBindHelper = new TISBindHelper(this, this::onTISConnected); - } - - private void onTISConnected(TISBinder binder) { - mTaskbarManager = binder.getTaskbarManager(); - mTaskbarManager.setActivity(this); - mOverviewCommandHelper = binder.getOverviewCommandHelper(); - } - - @Override - public void runOnBindToTouchInteractionService(Runnable r) { - mTISBindHelper.runOnBindToTouchInteractionService(r); - } - - private void initUnfoldTransitionProgressProvider() { - final UnfoldTransitionConfig config = UnfoldTransitionFactory.createConfig(this); - if (config.isEnabled()) { - mUnfoldTransitionProgressProvider = - UnfoldTransitionFactory.createUnfoldTransitionProgressProvider( - this, - config, - ProxyScreenStatusProvider.INSTANCE, - getSystemService(DeviceStateManager.class), - getSystemService(SensorManager.class), - getMainThreadHandler(), - getMainExecutor() - ); - - mLauncherUnfoldAnimationController = new LauncherUnfoldAnimationController( - this, - getWindowManager(), - mUnfoldTransitionProgressProvider - ); - } - } - - public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) { - mTaskbarUIController = taskbarUIController; - } - - public @Nullable LauncherTaskbarUIController getTaskbarUIController() { - return mTaskbarUIController; - } - - public T getActionsView() { - return (T) mActionsView; - } - - @Override - protected void closeOpenViews(boolean animate) { - super.closeOpenViews(animate); - TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); - } - - @Override - protected void collectStateHandlers(List out) { - super.collectStateHandlers(out); - out.add(getDepthController()); - out.add(new RecentsViewStateController(this)); - out.add(new BackButtonAlphaHandler(this)); - } - - public DepthController getDepthController() { - return mDepthController; - } - - @Nullable - public UnfoldTransitionProgressProvider getUnfoldTransitionProgressProvider() { - return mUnfoldTransitionProgressProvider; - } - - @Override - public boolean supportsAdaptiveIconAnimation(View clickedView) { - return mAppTransitionManager.hasControlRemoteAppTransitionPermission() - && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get(); - } - - @Override - public DragOptions getDefaultWorkspaceDragOptions() { - if (mNextWorkspaceDragOptions != null) { - DragOptions options = mNextWorkspaceDragOptions; - mNextWorkspaceDragOptions = null; - return options; - } - return super.getDefaultWorkspaceDragOptions(); - } - - public void setNextWorkspaceDragOptions(DragOptions dragOptions) { - mNextWorkspaceDragOptions = dragOptions; - } - - @Override - public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { - QuickstepTransitionManager appTransitionManager = getAppTransitionManager(); - appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() { - @Override - public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets) { - - // On the first call clear the reference. - signal.cancel(); - - ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0); - fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets, - wallpaperTargets)); - AnimatorSet anim = new AnimatorSet(); - anim.play(fadeAnimation); - return anim; - } - }, signal); - } - - @Override - public float[] getNormalOverviewScaleAndOffset() { - return SysUINavigationMode.getMode(this).hasGestures - ? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET}; - } - - @Override - public void onDragLayerHierarchyChanged() { - onLauncherStateOrFocusChanged(); - } - - @Override - protected void onActivityFlagsChanged(int changeBits) { - if ((changeBits - & (ACTIVITY_STATE_WINDOW_FOCUSED | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) { - onLauncherStateOrFocusChanged(); - } - - if ((changeBits & ACTIVITY_STATE_STARTED) != 0) { - mDepthController.setActivityStarted(isStarted()); - } - - if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) { - if (mTaskbarUIController != null) { - mTaskbarUIController.onLauncherResumedOrPaused(hasBeenResumed()); - } - } - - super.onActivityFlagsChanged(changeBits); - } - - public boolean shouldBackButtonBeHidden(LauncherState toState) { - Mode mode = SysUINavigationMode.getMode(this); - boolean shouldBackButtonBeHidden = mode.hasGestures - && toState.hasFlag(FLAG_HIDE_BACK_BUTTON) - && hasWindowFocus() - && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0; - if (shouldBackButtonBeHidden) { - // Show the back button if there is a floating view visible. - shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(this, - TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null; - } - return shouldBackButtonBeHidden; - } - - /** - * Sets the back button visibility based on the current state/window focus. - */ - private void onLauncherStateOrFocusChanged() { - boolean shouldBackButtonBeHidden = shouldBackButtonBeHidden(getStateManager().getState()); - if (SysUINavigationMode.getMode(this) == TWO_BUTTONS) { - UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA, - shouldBackButtonBeHidden ? 0f : 1f, true /* animate */); - } - if (getDragLayer() != null) { - getRootView().setDisallowBackGesture(shouldBackButtonBeHidden); - } - } - - @Override - public void finishBindingItems(IntSet pagesBoundFirst) { - super.finishBindingItems(pagesBoundFirst); - // Instantiate and initialize WellbeingModel now that its loading won't interfere with - // populating workspace. - // TODO: Find a better place for this - WellbeingModel.INSTANCE.get(this); - } - - @Override - public Stream getSupportedShortcuts() { - return Stream.concat(Stream.of(WellbeingModel.SHORTCUT_FACTORY), - super.getSupportedShortcuts()); - } - - @Override - public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { - ActivityOptionsWrapper activityOptions = - mAppTransitionManager.hasControlRemoteAppTransitionPermission() - ? mAppTransitionManager.getActivityLaunchOptions(v) - : super.getActivityLaunchOptions(v, item); - if (mLastTouchUpTime > 0) { - ActivityOptionsCompat.setLauncherSourceInfo( - activityOptions.options, mLastTouchUpTime); - } - if (Utilities.ATLEAST_S && LawnchairApp.isRecentsEnabled()) { - activityOptions.options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); - addLaunchCookie(item, activityOptions.options); - } - return activityOptions; - } - - /** - * 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 LauncherSettings.Favorites.CONTAINER_DESKTOP: - case LauncherSettings.Favorites.CONTAINER_HOTSEAT: - // 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 LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: - case LauncherSettings.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); - // When changing screens, force moving to rest state similar to StatefulActivity.onStop, as - // StatefulActivity isn't called consistently. - if ((flags & CHANGE_ACTIVE_SCREEN) != 0) { - getStateManager().moveToRestState(); - } - } - - @Override - public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { - super.dump(prefix, fd, writer, args); - if (mDepthController != null) { - mDepthController.dump(prefix, writer); - } - } - - @Override - public void updateWindowInsets(WindowInsets.Builder updatedInsetsBuilder, - WindowInsets oldInsets) { - // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar - // would count towards it). This is used for the bottom protection in All Apps for example. - if (SysUINavigationMode.getMode(this) == NO_BUTTON) { - Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); - Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, - oldTappableInsets.right, 0); - updatedInsetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); - } - } -} diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index 661053af18..9f9f2c8654 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -16,9 +16,9 @@ package com.android.launcher3; import static com.android.launcher3.Utilities.postAsyncCallback; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; 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.window.RefreshRateTracker.getSingleFrameMs; import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; import android.animation.Animator; @@ -28,13 +28,16 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Handler; +import android.os.RemoteException; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationTarget; import androidx.annotation.BinderThread; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.android.systemui.animation.RemoteAnimationDelegate; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import java.lang.ref.WeakReference; @@ -55,7 +58,7 @@ import java.lang.ref.WeakReference; * reference to the runner, leaving only the weak ref from the runner. */ @TargetApi(Build.VERSION_CODES.P) -public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { +public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { private static final RemoteAnimationFactory DEFAULT_FACTORY = (transit, appTargets, wallpaperTargets, nonAppTargets, result) -> @@ -82,14 +85,14 @@ public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { @BinderThread public void onAnimationStart( int transit, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, Runnable runnable) { Runnable r = () -> { finishExistingAnimation(); mAnimationResult = new AnimationResult(() -> mAnimationResult = null, runnable); - getFactory().onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets, + getFactory().onAnimationStart(transit, appTargets, wallpaperTargets, nonAppTargets, mAnimationResult); }; if (mStartAtFrontOfQueue) { @@ -99,22 +102,6 @@ public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { } } - // Called only in R platform - @BinderThread - public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) { - onAnimationStart(0 /* transit */, appTargets, wallpaperTargets, - new RemoteAnimationTargetCompat[0], runnable); - } - - // Called only in Q platform - @BinderThread - @Deprecated - public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets, Runnable runnable) { - onAnimationStart(appTargets, new RemoteAnimationTargetCompat[0], runnable); - } - - private RemoteAnimationFactory getFactory() { RemoteAnimationFactory factory = mFactory.get(); return factory != null ? factory : DEFAULT_FACTORY; @@ -133,14 +120,18 @@ public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { */ @BinderThread @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { postAsyncCallback(mHandler, () -> { finishExistingAnimation(); getFactory().onAnimationCancelled(); }); } - public static final class AnimationResult { + /** + * Used by RemoteAnimationFactory implementations to run the actual animation and its lifecycle + * callbacks. + */ + public static final class AnimationResult extends IRemoteAnimationFinishedCallback.Stub { private final Runnable mSyncFinishRunnable; private final Runnable mASyncFinishRunnable; @@ -215,25 +206,41 @@ public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { } } } + + /** + * When used as a simple IRemoteAnimationFinishedCallback, this method is used to run the + * animation finished runnable. + */ + @Override + public void onAnimationFinished() throws RemoteException { + mASyncFinishRunnable.run(); + } } /** * Used with LauncherAnimationRunner as an interface for the runner to call back to the * implementation. */ - @FunctionalInterface - public interface RemoteAnimationFactory { + public interface RemoteAnimationFactory extends RemoteAnimationDelegate { /** * Called on the UI thread when the animation targets are received. The implementation must * call {@link AnimationResult#setAnimation} with the target animation to be run. */ - void onCreateAnimation(int transit, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, + @Override + @UiThread + void onAnimationStart(int transit, + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, LauncherAnimationRunner.AnimationResult result); + @Override + @UiThread + default void onAnimationCancelled(boolean isKeyguardOccluded) { + onAnimationCancelled(); + } + /** * Called when the animation is cancelled. This can happen with or without * the create being called. diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java index 35151f1a68..28bd701a48 100644 --- a/quickstep/src/com/android/launcher3/LauncherInitListener.java +++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java @@ -19,10 +19,11 @@ import android.animation.AnimatorSet; import android.annotation.TargetApi; import android.os.Build; import android.os.CancellationSignal; +import android.view.RemoteAnimationTarget; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.quickstep.util.ActivityInitListener; import com.android.quickstep.util.RemoteAnimationProvider; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import java.util.function.BiPredicate; @@ -44,15 +45,15 @@ public class LauncherInitListener extends ActivityInitListener { public boolean handleInit(Launcher launcher, boolean alreadyOnHome) { if (mRemoteAnimationProvider != null) { QuickstepTransitionManager appTransitionManager = - ((BaseQuickstepLauncher) launcher).getAppTransitionManager(); + ((QuickstepLauncher) launcher).getAppTransitionManager(); // Set a one-time animation provider. After the first call, this will get cleared. // TODO: Probably also check the intended target id. CancellationSignal cancellationSignal = new CancellationSignal(); appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() { @Override - public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets) { + public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets) { // On the first call clear the reference. cancellationSignal.cancel(); diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java index 96559cbb8a..962fd91c2e 100644 --- a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java +++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java @@ -44,7 +44,7 @@ public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegat @Override protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { - QuickstepLauncher launcher = (QuickstepLauncher) mLauncher; + QuickstepLauncher launcher = (QuickstepLauncher) mContext; if (action == PIN_PREDICTION) { if (launcher.getHotseatPredictionController() == null) { return false; diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 2c3d02e800..0868e0a73f 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -16,8 +16,19 @@ package com.android.launcher3; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; +import static android.window.TransitionFilter.CONTAINER_ORDER_TOP; import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; @@ -27,9 +38,9 @@ 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.Utilities.postAsyncCallback; import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5; @@ -40,24 +51,23 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_BACK_SWIPE_HOME_A 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.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS; import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; -import static com.android.launcher3.statehandlers.DepthController.DEPTH; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; +import static com.android.launcher3.util.DisplayController.isTransientTaskbar; +import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +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.TaskViewUtils.findTaskViewToLaunch; import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.app.ActivityOptions; +import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -77,16 +87,23 @@ import android.os.Looper; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; -import android.util.Log; import android.util.Pair; import android.util.Size; +import android.view.CrossWindowBlurListeners; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationDefinition; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.window.RemoteTransition; +import android.window.TransitionFilter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -95,47 +112,49 @@ import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory; import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.icons.FastBitmapDrawable; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.taskbar.LauncherTaskbarUIController; 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.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.RunnableList; 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.MultiValueUpdateListener; import com.android.quickstep.util.RectFSpringAnim; +import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig; +import com.android.quickstep.util.RectFSpringAnim.TaskbarHotseatSpringConfig; import com.android.quickstep.util.RemoteAnimationProvider; import com.android.quickstep.util.StaggeredWorkspaceAnim; +import com.android.quickstep.util.SurfaceTransaction; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.WorkspaceRevealAnim; import com.android.quickstep.views.FloatingWidgetView; import com.android.quickstep.views.RecentsView; -import com.android.systemui.shared.system.ActivityCompat; -import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DelegateLaunchAnimatorController; +import com.android.systemui.animation.RemoteAnimationDelegate; import com.android.systemui.shared.system.BlurUtils; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; -import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.RemoteTransitionCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; -import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.shared.testing.ResourceUtils; import com.android.wm.shell.startingsurface.IStartingWindowListener; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -148,22 +167,20 @@ import app.lawnchair.theme.color.ColorTokens; */ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener { - private static final String TAG = "QuickstepTransition"; - - private static final boolean ENABLE_SHELL_STARTING_SURFACE = - SystemProperties.getBoolean("persist.debug.shell_starting_surface", true); + private static final boolean ENABLE_SHELL_STARTING_SURFACE = SystemProperties + .getBoolean("persist.debug.shell_starting_surface", true); /** Duration of status bar animations. */ public static final int STATUS_BAR_TRANSITION_DURATION = 120; /** - * Since our animations decelerate heavily when finishing, we want to start status bar + * Since our animations decelerate heavily when finishing, we want to start + * status bar * animations x ms before the ending. */ public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96; - private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = - "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; + private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; private static final long APP_LAUNCH_DURATION = 500; @@ -172,12 +189,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public static final int ANIMATION_NAV_FADE_IN_DURATION = 266; public static final int ANIMATION_NAV_FADE_OUT_DURATION = 133; - public static final long ANIMATION_DELAY_NAV_FADE_IN = - APP_LAUNCH_DURATION - ANIMATION_NAV_FADE_IN_DURATION; - public static final Interpolator NAV_FADE_IN_INTERPOLATOR = - new PathInterpolator(0f, 0f, 0f, 1f); - public static final Interpolator NAV_FADE_OUT_INTERPOLATOR = - new PathInterpolator(0.2f, 0f, 1f, 1f); + public static final long ANIMATION_DELAY_NAV_FADE_IN = APP_LAUNCH_DURATION - ANIMATION_NAV_FADE_IN_DURATION; + public static final Interpolator NAV_FADE_IN_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); + public static final Interpolator NAV_FADE_OUT_INTERPOLATOR = new PathInterpolator(0.2f, 0f, 1f, 1f); public static final int RECENTS_LAUNCH_DURATION = 336; private static final int LAUNCHER_RESUME_START_DELAY = 100; @@ -186,6 +200,12 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public static final int SPLIT_DIVIDER_ANIM_DURATION = 100; public static final int CONTENT_ALPHA_DURATION = 217; + public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417; + public static final int TASKBAR_TO_APP_DURATION = 600; + // TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with + // unlock animation + // is solved. + public static final int TASKBAR_TO_HOME_DURATION = 300; protected static final int CONTENT_SCALE_DURATION = 350; protected static final int CONTENT_SCRIM_DURATION = 350; @@ -194,14 +214,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Cross-fade duration between App Widget and App private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125; - protected final BaseQuickstepLauncher mLauncher; + protected final QuickstepLauncher mLauncher; + protected final DragLayer mDragLayer; - private final DragLayer mDragLayer; - private final AlphaProperty mDragLayerAlpha; + protected final Handler mHandler; - final Handler mHandler; - - private final float mContentScale; private final float mClosingWindowTransY; private final float mMaxShadowRadius; @@ -210,14 +227,15 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener private DeviceProfile mDeviceProfile; private RemoteAnimationProvider mRemoteAnimationProvider; - // Strong refs to runners which are cleared when the launcher activity is destroyed + // 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 RemoteTransitionCompat mLauncherOpenTransition; + private RemoteTransition mLauncherOpenTransition; + private LauncherBackAnimationController mBackAnimationController; private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -230,7 +248,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }; - // Pairs of window starting type and starting window background color for starting tasks + // Pairs of window starting type and starting window background color for + // starting tasks // Will never be larger than MAX_NUM_TASKS private LinkedHashMap> mTaskStartParams; @@ -240,12 +259,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public QuickstepTransitionManager(Context context) { mLauncher = Launcher.cast(Launcher.getLauncher(context)); mDragLayer = mLauncher.getDragLayer(); - mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS); mHandler = new Handler(Looper.getMainLooper()); mDeviceProfile = mLauncher.getDeviceProfile(); + mBackAnimationController = new LauncherBackAnimationController(mLauncher, this); Resources res = mLauncher.getResources(); - mContentScale = res.getFloat(R.dimen.content_scale); mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius); @@ -275,17 +293,28 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * @return ActivityOptions with remote animations that controls how the window of the opening - * targets are displayed. + * @return ActivityOptions with remote animations that controls how the window + * of the opening + * targets are displayed. */ public ActivityOptionsWrapper getActivityLaunchOptions(View v) { boolean fromRecents = isLaunchingFromRecents(v, null /* targets */); RunnableList onEndCallback = new RunnableList(); - mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback); - RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner( - mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); - // Note that this duration is a guess as we do not know if the animation will be a + RemoteAnimationFactory delegateRunner = new AppLaunchAnimationRunner(v, onEndCallback); + ItemInfo tag = (ItemInfo) v.getTag(); + if (tag != null && tag.shouldUseBackgroundAnimation()) { + ContainerAnimationRunner containerAnimationRunner = ContainerAnimationRunner.from(v, + mStartingWindowListener, onEndCallback); + if (containerAnimationRunner != null) { + delegateRunner = containerAnimationRunner; + } + } + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner( + mHandler, delegateRunner, true /* startAtFrontOfQueue */); + + // 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. long duration = fromRecents ? RECENTS_LAUNCH_DURATION @@ -293,25 +322,29 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION - STATUS_BAR_TRANSITION_PRE_DELAY; - RemoteAnimationAdapterCompat adapterCompat = - new RemoteAnimationAdapterCompat(runner, duration, statusBarTransitionDelay, - mLauncher.getIApplicationThread()); - return new ActivityOptionsWrapper( - ActivityOptionsCompat.makeRemoteAnimation(adapterCompat), onEndCallback); + ActivityOptions options = ActivityOptions.makeRemoteAnimation( + new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay), + new RemoteTransition(runner.toRemoteTransition(), + mLauncher.getIApplicationThread())); + return new ActivityOptionsWrapper(options, onEndCallback); } /** - * 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 - * may not always be correct as we may resolve the opening app to a task when the animation + * 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 + * may not always be correct as we may resolve the opening app to a task when + * the animation * starts. * * @param v the view to launch from * @param targets apps that are opening/closing - * @return true if the app is launching from recents, false if it most likely is not + * @return true if the app is launching from recents, false if it most likely is + * not */ protected boolean isLaunchingFromRecents(@NonNull View v, - @Nullable RemoteAnimationTargetCompat[] targets) { + @Nullable RemoteAnimationTarget[] targets) { return mLauncher.getStateManager().getState().overviewUi && findTaskViewToLaunch(mLauncher.getOverviewPanel(), v, targets) != null; } @@ -325,22 +358,23 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param launcherClosing true if the launcher app is closing */ protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, - @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing) { + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing, mLauncher.getStateManager(), mLauncher.getOverviewPanel(), mLauncher.getDepthController()); } - private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTargetCompat[] targets) { + private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTarget[] targets) { boolean isAllOpeningTargetTrs = true; for (int i = 0; i < targets.length; i++) { - RemoteAnimationTargetCompat target = targets[i]; + RemoteAnimationTarget target = targets[i]; if (target.mode == MODE_OPENING) { isAllOpeningTargetTrs &= target.isTranslucent; } - if (!isAllOpeningTargetTrs) break; + if (!isAllOpeningTargetTrs) + break; } return isAllOpeningTargetTrs; } @@ -354,27 +388,24 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param launcherClosing true if launcher is closing */ private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, - @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets, + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { // Set the state animation first so that any state listeners are called // before our internal listeners. mLauncher.getStateManager().setCurrentAnimation(anim); - final int rotationChange = getRotationChange(appTargets); // Note: the targetBounds are relative to the launcher int startDelay = getSingleFrameMs(mLauncher); - Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange); - Animator windowAnimator = getOpeningWindowAnimators(v, appTargets, wallpaperTargets, - nonAppTargets, windowTargetBounds, areAllTargetsTranslucent(appTargets), - rotationChange); + Animator windowAnimator = getOpeningWindowAnimators( + v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing); windowAnimator.setStartDelay(startDelay); anim.play(windowAnimator); if (launcherClosing) { // Delay animation by a frame to avoid jank. - Pair launcherContentAnimator = - getLauncherContentAnimator(true /* isAppOpening */, startDelay, false); + Pair launcherContentAnimator = getLauncherContentAnimator(true /* isAppOpening */, + startDelay, false); anim.play(launcherContentAnimator.first); anim.addListener(new AnimatorListenerAdapter() { @Override @@ -382,56 +413,38 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener launcherContentAnimator.second.run(); } }); - } else { - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mLauncher.addOnResumeCallback(() -> - ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH, - mLauncher.getStateManager().getState().getDepth( - mLauncher)).start()); - } - }); } } private void composeWidgetLaunchAnimator( @NonNull AnimatorSet anim, @NonNull LauncherAppWidgetHostView v, - @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets) { + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, + boolean launcherClosing) { mLauncher.getStateManager().setCurrentAnimation(anim); - - Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets)); - anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets, - windowTargetBounds, areAllTargetsTranslucent(appTargets))); - - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mLauncher.addOnResumeCallback(() -> - ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH, - mLauncher.getStateManager().getState().getDepth( - mLauncher)).start()); - } - }); + anim.play(getOpeningWindowAnimatorsForWidget( + v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing)); } /** * Return the window bounds of the opening target. - * In multiwindow mode, we need to get the final size of the opening app window target to help + * In multiwindow mode, we need to get the final size of the opening app window + * target to help * figure out where the floating view should animate to. */ - private Rect getWindowTargetBounds(@NonNull RemoteAnimationTargetCompat[] appTargets, + private Rect getWindowTargetBounds(@NonNull RemoteAnimationTarget[] appTargets, int rotationChange) { - RemoteAnimationTargetCompat target = null; - for (RemoteAnimationTargetCompat t : appTargets) { - if (t.mode != MODE_OPENING) continue; + RemoteAnimationTarget target = null; + for (RemoteAnimationTarget t : appTargets) { + if (t.mode != MODE_OPENING) + continue; target = t; break; } - if (target == null) return new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + if (target == null) + return new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); final Rect bounds = new Rect(target.screenSpaceBounds); if (target.localBounds != null) { bounds.set(target.localBounds); @@ -448,7 +461,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 4 - rotationChange); } } - if (mDeviceProfile.isTaskbarPresentInApps) { + if (mDeviceProfile.isTaskbarPresentInApps + && !target.willShowImeOnTarget + && !isTransientTaskbar(mLauncher)) { // Animate to above the taskbar. bounds.bottom -= target.contentInsets.bottom; } @@ -466,11 +481,12 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Content is everything on screen except the background and the floating view (if any). + * Content is everything on screen except the background and the floating view + * (if any). * - * @param isAppOpening True when this is called when an app is opening. - * False when this is called when an app is closing. - * @param startDelay Start delay duration. + * @param isAppOpening True when this is called when an app is opening. + * False when this is called when an app is closing. + * @param startDelay Start delay duration. * @param skipAllAppsScale True if we want to avoid scaling All Apps */ private Pair getLauncherContentAnimator(boolean isAppOpening, @@ -479,18 +495,27 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener Runnable endListener; float[] alphas = isAppOpening - ? new float[]{1, 0} - : new float[]{0, 1}; + ? new float[] { 1, 0 } + : new float[] { 0, 1 }; float[] scales = isAppOpening - ? new float[]{1, mContentScale} - : new float[]{mContentScale, 1}; + ? new float[] { 1, mDeviceProfile.workspaceContentScale } + : new float[] { mDeviceProfile.workspaceContentScale, 1 }; + + // Pause expensive view updates as they can lead to layer thrashing and skipped + // frames. + mLauncher.pauseExpensiveViewUpdates(); if (mLauncher.isInState(ALL_APPS)) { - // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView. + // All Apps in portrait mode is full screen, so we only animate + // AllAppsContainerView. final View appsView = mLauncher.getAppsView(); final float startAlpha = appsView.getAlpha(); final float startScale = SCALE_PROPERTY.get(appsView); + if (mDeviceProfile.isTablet) { + // AllApps should not fade at all in tablets. + alphas = new float[] { 1, 1 }; + } appsView.setAlpha(alphas[0]); ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); @@ -518,16 +543,26 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener appsView.setAlpha(startAlpha); SCALE_PROPERTY.set(appsView, startScale); appsView.setLayerType(View.LAYER_TYPE_NONE, null); + mLauncher.resumeExpensiveViewUpdates(); }; } else if (mLauncher.isInState(OVERVIEW)) { endListener = composeViewContentAnimator(launcherAnimator, alphas, scales); } else { List viewsToAnimate = new ArrayList<>(); - Workspace workspace = mLauncher.getWorkspace(); + Workspace workspace = mLauncher.getWorkspace(); workspace.forEachVisiblePage( view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets())); - viewsToAnimate.add(mLauncher.getHotseat()); + // Do not scale hotseat as a whole when taskbar is present, and scale QSB only + // if it's + // not inline. + if (mDeviceProfile.isTaskbarPresent) { + if (!mDeviceProfile.isQsbInline) { + viewsToAnimate.add(mLauncher.getHotseat().getQsb()); + } + } else { + viewsToAnimate.add(mLauncher.getHotseat()); + } viewsToAnimate.forEach(view -> { view.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -546,8 +581,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener : ColorTokens.OverviewScrim.resolveColor(mLauncher); int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0); int[] colors = isAppOpening - ? new int[]{scrimColorTrans, scrimColor} - : new int[]{scrimColor, scrimColorTrans}; + ? new int[] { scrimColorTrans, scrimColor } + : new int[] { scrimColor, scrimColorTrans }; ScrimView scrimView = mLauncher.getScrimView(); if (scrimView.getBackground() instanceof ColorDrawable) { scrimView.setBackgroundColor(colors[0]); @@ -557,36 +592,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener scrim.setDuration(CONTENT_SCRIM_DURATION); scrim.setInterpolator(DEACCEL_1_5); - if (useTaskbarColor) { - // Hide the taskbar background color since it would duplicate the scrim. - scrim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - LauncherTaskbarUIController taskbarUIController = - mLauncher.getTaskbarUIController(); - if (taskbarUIController != null) { - taskbarUIController.forceHideBackground(true); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - LauncherTaskbarUIController taskbarUIController = - mLauncher.getTaskbarUIController(); - if (taskbarUIController != null) { - taskbarUIController.forceHideBackground(false); - } - } - }); - } - launcherAnimator.play(scrim); } } - // Pause page indicator animations as they lead to layer trashing. - mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); - endListener = () -> { viewsToAnimate.forEach(view -> { SCALE_PROPERTY.set(view, 1f); @@ -595,7 +604,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (scrimEnabled) { mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT); } - mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd(); + mLauncher.resumeExpensiveViewUpdates(); }; } @@ -604,7 +613,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Compose recents view alpha and translation Y animation when launcher opens/closes apps. + * Compose recents view alpha and translation Y animation when launcher + * opens/closes apps. * * @param anim the animator set to add to * @param alphas the alphas to animate to over time @@ -616,28 +626,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener RecentsView overview = mLauncher.getOverviewPanel(); ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, RecentsView.CONTENT_ALPHA, alphas); - Log.d(BAD_STATE, "QTM composeViewContentAnimator alphas=" + Arrays.toString(alphas)); - alpha.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - Log.d(BAD_STATE, "QTM composeViewContentAnimator onStart"); - } - - @Override - public void onAnimationCancel(Animator animation) { - float alpha = overview == null ? -1 : RecentsView.CONTENT_ALPHA.get(overview); - Log.d(BAD_STATE, "QTM composeViewContentAnimator onCancel, alpha=" + alpha); - } - - @Override - public void onAnimationEnd(Animator animation) { - Log.d(BAD_STATE, "QTM composeViewContentAnimator onEnd"); - } - }); alpha.setDuration(CONTENT_ALPHA_DURATION); alpha.setInterpolator(LINEAR); anim.play(alpha); - Log.d(BAD_STATE, "QTM composeViewContentAnimator setFreezeVisibility=true"); overview.setFreezeViewVisibility(true); ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(overview, SCALE_PROPERTY, scales); @@ -646,33 +637,41 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener anim.play(scaleAnim); return () -> { - Log.d(BAD_STATE, "QTM composeViewContentAnimator onEnd setFreezeVisibility=false"); overview.setFreezeViewVisibility(false); SCALE_PROPERTY.set(overview, 1f); mLauncher.getStateManager().reapplyState(); + mLauncher.resumeExpensiveViewUpdates(); }; } /** - * @return Animator that controls the window of the opening targets from app icons. + * @return Animator that controls the window of the opening targets from app + * icons. */ private Animator getOpeningWindowAnimators(View v, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, - Rect windowTargetBounds, boolean appTargetsAreTranslucent, int rotationChange) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, + boolean launcherClosing) { + int rotationChange = getRotationChange(appTargets); + Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange); + boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets); + RectF launcherIconBounds = new RectF(); - FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v, - !appTargetsAreTranslucent, launcherIconBounds, true /* isOpening */); + FloatingIconView floatingView = getFloatingIconView(mLauncher, v, + (mLauncher.getTaskbarUIController() == null || !isTransientTaskbar(mLauncher)) + ? null + : mLauncher.getTaskbarUIController().findMatchingView(v), + null /* fadeOutView */, !appTargetsAreTranslucent, launcherIconBounds, + true /* isOpening */); Rect crop = new Rect(); Matrix matrix = new Matrix(); RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets, MODE_OPENING); - SurfaceTransactionApplier surfaceApplier = - new SurfaceTransactionApplier(floatingView); + SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView); openingTargets.addReleaseCheck(surfaceApplier); - RemoteAnimationTargetCompat navBarTarget = openingTargets.getNavBarRemoteAnimationTarget(); + RemoteAnimationTarget navBarTarget = openingTargets.getNavBarRemoteAnimationTarget(); int[] dragLayerBounds = new int[2]; mDragLayer.getLocationOnScreen(dragLayerBounds); @@ -681,8 +680,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (supportsSSplashScreen()) { int taskId = openingTargets.getFirstAppTargetTaskId(); Pair defaultParams = Pair.create(STARTING_WINDOW_TYPE_NONE, 0); - Pair taskParams = - mTaskStartParams.getOrDefault(taskId, defaultParams); + Pair taskParams = mTaskStartParams.getOrDefault(taskId, defaultParams); mTaskStartParams.remove(taskId); hasSplashScreen = taskParams.first == STARTING_WINDOW_TYPE_SPLASH_SCREEN; } else { @@ -709,6 +707,18 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener appAnimator.setInterpolator(LINEAR); appAnimator.addListener(floatingView); appAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController(); + if (taskbarController != null && taskbarController.shouldShowEduOnAppLaunch()) { + // LAUNCHER_TASKBAR_EDUCATION_SHOWING is set to true here, when the education + // flow is about to start, to avoid a race condition with other components + // that would show something else to the user as soon as the app is opened. + Settings.Secure.putInt(mLauncher.getContentResolver(), + LAUNCHER_TASKBAR_EDUCATION_SHOWING, 1); + } + } + @Override public void onAnimationEnd(Animator animation) { if (v instanceof BubbleTextView) { @@ -716,7 +726,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController(); if (taskbarController != null) { - taskbarController.showEdu(); + taskbarController.showEduOnAppLaunch(); } openingTargets.release(); } @@ -726,7 +736,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener ? Math.max(crop.width(), crop.height()) / 2f * IconShapeManager.getWindowTransitionRadius(mLauncher) : 0f; final float finalWindowRadius = mDeviceProfile.isMultiWindowMode - ? 0 : getWindowCornerRadius(mLauncher); + ? 0 + : getWindowCornerRadius(mLauncher); final float finalShadowRadius = appTargetsAreTranslucent ? 0 : mMaxShadowRadius; MultiValueUpdateListener listener = new MultiValueUpdateListener() { @@ -807,15 +818,16 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (initOnly) { // For the init pass, we want full alpha since the window is not yet ready. - floatingView.update(1f, 255, floatingIconBounds, percent, 0f, + floatingView.update(1f, floatingIconBounds, percent, 0f, mWindowRadius.value * scale, true /* isOpening */); return; } - ArrayList params = new ArrayList<>(); + SurfaceTransaction transaction = new SurfaceTransaction(); + for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = appTargets[i]; - SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash); + RemoteAnimationTarget target = appTargets[i]; + SurfaceProperties builder = transaction.forSurface(target.leash); if (target.mode == MODE_OPENING) { matrix.setScale(scale, scale); @@ -834,16 +846,15 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener matrix.postTranslate(windowTransX0, windowTransY0); } - floatingView.update(mIconAlpha.value, 255, floatingIconBounds, percent, 0f, + floatingView.update(mIconAlpha.value, floatingIconBounds, percent, 0f, mWindowRadius.value * scale, true /* isOpening */); - builder.withMatrix(matrix) - .withWindowCrop(crop) - .withAlpha(1f - mIconAlpha.value) - .withCornerRadius(mWindowRadius.value) - .withShadowRadius(mShadowRadius.value); + builder.setMatrix(matrix) + .setWindowCrop(crop) + .setAlpha(1f - mIconAlpha.value) + .setCornerRadius(mWindowRadius.value) + .setShadowRadius(mShadowRadius.value); } else if (target.mode == MODE_CLOSING) { if (target.localBounds != null) { - final Rect localBounds = target.localBounds; tmpPos.set(target.localBounds.left, target.localBounds.top); } else { tmpPos.set(target.position.x, target.position.y); @@ -860,38 +871,36 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener tmpPos.y = tmp; } matrix.setTranslate(tmpPos.x, tmpPos.y); - builder.withMatrix(matrix) - .withWindowCrop(crop) - .withAlpha(1f); + builder.setMatrix(matrix) + .setWindowCrop(crop) + .setAlpha(1f); } - params.add(builder.build()); } if (navBarTarget != null) { - final SurfaceParams.Builder navBuilder = - new SurfaceParams.Builder(navBarTarget.leash); + SurfaceProperties navBuilder = transaction.forSurface(navBarTarget.leash); if (mNavFadeIn.value > mNavFadeIn.getStartValue()) { matrix.setScale(scale, scale); matrix.postTranslate(windowTransX0, windowTransY0); - navBuilder.withMatrix(matrix) - .withWindowCrop(crop) - .withAlpha(mNavFadeIn.value); + navBuilder.setMatrix(matrix) + .setWindowCrop(crop) + .setAlpha(mNavFadeIn.value); } else { - navBuilder.withAlpha(mNavFadeOut.value); + navBuilder.setAlpha(mNavFadeOut.value); } - params.add(navBuilder.build()); } - - surfaceApplier.scheduleApply(params.toArray(new SurfaceParams[params.size()])); + surfaceApplier.scheduleApply(transaction); } }; appAnimator.addUpdateListener(listener); - // Since we added a start delay, call update here to init the FloatingIconView properly. + // Since we added a start delay, call update here to init the FloatingIconView + // properly. listener.onUpdate(0, true /* initOnly */); - // If app targets are translucent, do not animate the background as it causes a visible + // If app targets are translucent, do not animate the background as it causes a + // visible // flicker when it resets itself at the end of its animation. - if (appTargetsAreTranslucent) { + if (appTargetsAreTranslucent || !launcherClosing) { animatorSet.play(appAnimator); } else { animatorSet.playTogether(appAnimator, getBackgroundAnimator()); @@ -900,41 +909,45 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, Rect windowTargetBounds, - boolean appTargetsAreTranslucent) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { + Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets)); + boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets); + final RectF widgetBackgroundBounds = new RectF(); final Rect appWindowCrop = new Rect(); final Matrix matrix = new Matrix(); RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets, MODE_OPENING); - RemoteAnimationTargetCompat openingTarget = openingTargets.getFirstAppTarget(); + RemoteAnimationTarget openingTarget = openingTargets.getFirstAppTarget(); int fallbackBackgroundColor = 0; if (openingTarget != null && supportsSSplashScreen()) { fallbackBackgroundColor = mTaskStartParams.containsKey(openingTarget.taskId) - ? mTaskStartParams.get(openingTarget.taskId).second : 0; + ? mTaskStartParams.get(openingTarget.taskId).second + : 0; mTaskStartParams.remove(openingTarget.taskId); } if (fallbackBackgroundColor == 0) { - fallbackBackgroundColor = - FloatingWidgetView.getDefaultBackgroundColor(mLauncher, openingTarget); + fallbackBackgroundColor = FloatingWidgetView.getDefaultBackgroundColor(mLauncher, openingTarget); } final float finalWindowRadius = mDeviceProfile.isMultiWindowMode - ? 0 : getWindowCornerRadius(mLauncher); + ? 0 + : getWindowCornerRadius(mLauncher); final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher, v, widgetBackgroundBounds, new Size(windowTargetBounds.width(), windowTargetBounds.height()), finalWindowRadius, appTargetsAreTranslucent, fallbackBackgroundColor); final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources()) - ? floatingView.getInitialCornerRadius() : 0; + ? floatingView.getInitialCornerRadius() + : 0; SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView); openingTargets.addReleaseCheck(surfaceApplier); - RemoteAnimationTargetCompat navBarTarget = openingTargets.getNavBarRemoteAnimationTarget(); + RemoteAnimationTarget navBarTarget = openingTargets.getNavBarRemoteAnimationTarget(); AnimatorSet animatorSet = new AnimatorSet(); ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); @@ -992,49 +1005,46 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener mAppWindowScale = widgetBackgroundBounds.width() / windowTargetBounds.width(); // Crop scaled app window to match widget appWindowCrop.set(0 /* left */, 0 /* top */, - Math.round(windowTargetBounds.width()) /* right */, + windowTargetBounds.width() /* right */, Math.round(widgetBackgroundBounds.height() / mAppWindowScale) /* bottom */); matrix.setTranslate(widgetBackgroundBounds.left, widgetBackgroundBounds.top); matrix.postScale(mAppWindowScale, mAppWindowScale, widgetBackgroundBounds.left, widgetBackgroundBounds.top); - ArrayList params = new ArrayList<>(); + SurfaceTransaction transaction = new SurfaceTransaction(); float floatingViewAlpha = appTargetsAreTranslucent ? 1 - mPreviewAlpha.value : 1; for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = appTargets[i]; - SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash); + RemoteAnimationTarget target = appTargets[i]; + SurfaceProperties builder = transaction.forSurface(target.leash); if (target.mode == MODE_OPENING) { floatingView.update(widgetBackgroundBounds, floatingViewAlpha, mWidgetForegroundAlpha.value, mWidgetFallbackBackgroundAlpha.value, mCornerRadiusProgress.value); - builder.withMatrix(matrix) - .withWindowCrop(appWindowCrop) - .withAlpha(mPreviewAlpha.value) - .withCornerRadius(mWindowRadius.value / mAppWindowScale); + builder.setMatrix(matrix) + .setWindowCrop(appWindowCrop) + .setAlpha(mPreviewAlpha.value) + .setCornerRadius(mWindowRadius.value / mAppWindowScale); } - params.add(builder.build()); } if (navBarTarget != null) { - final SurfaceParams.Builder navBuilder = - new SurfaceParams.Builder(navBarTarget.leash); + SurfaceProperties navBuilder = transaction.forSurface(navBarTarget.leash); if (mNavFadeIn.value > mNavFadeIn.getStartValue()) { - navBuilder.withMatrix(matrix) - .withWindowCrop(appWindowCrop) - .withAlpha(mNavFadeIn.value); + navBuilder.setMatrix(matrix) + .setWindowCrop(appWindowCrop) + .setAlpha(mNavFadeIn.value); } else { - navBuilder.withAlpha(mNavFadeOut.value); + navBuilder.setAlpha(mNavFadeOut.value); } - params.add(navBuilder.build()); } - - surfaceApplier.scheduleApply(params.toArray(new SurfaceParams[params.size()])); + surfaceApplier.scheduleApply(transaction); } }); - // If app targets are translucent, do not animate the background as it causes a visible + // If app targets are translucent, do not animate the background as it causes a + // visible // flicker when it resets itself at the end of its animation. - if (appTargetsAreTranslucent) { + if (appTargetsAreTranslucent || !launcherClosing) { animatorSet.play(appAnimator); } else { animatorSet.playTogether(appAnimator, getBackgroundAnimator()); @@ -1046,56 +1056,42 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * Returns animator that controls depth/blur of the background. */ private ObjectAnimator getBackgroundAnimator() { - // When launching an app from overview that doesn't map to a task, we still want to just + // When launching an app from overview that doesn't map to a task, we still want + // to just // blur the wallpaper instead of the launcher surface as well - boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW; - DepthController depthController = mLauncher.getDepthController(); - ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController, DEPTH, - BACKGROUND_APP.getDepth(mLauncher)) + boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW + && BlurUtils.supportsBlursOnWindows(); + + MyDepthController depthController = new MyDepthController(mLauncher); + ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController.stateDepth, + MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mLauncher)) .setDuration(APP_LAUNCH_DURATION); + if (allowBlurringLauncher) { - final SurfaceControl dimLayer; - if (BlurUtils.supportsBlursOnWindows()) { - // Create a temporary effect layer, that lives on top of launcher, so we can apply - // the blur to it. The EffectLayer will be fullscreen, which will help with caching - // optimizations on the SurfaceFlinger side: - // - Results would be able to be cached as a texture - // - There won't be texture allocation overhead, because EffectLayers don't have - // buffers - ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl(); - SurfaceControl parent = viewRootImpl != null - ? viewRootImpl.getSurfaceControl() - : null; - dimLayer = new SurfaceControl.Builder() - .setName("Blur layer") - .setParent(parent) - .setOpaque(false) - .setHidden(false) - .setEffectLayer() - .build(); - } else { - dimLayer = null; - } + // Create a temporary effect layer, that lives on top of launcher, so we can + // apply + // the blur to it. The EffectLayer will be fullscreen, which will help with + // caching + // optimizations on the SurfaceFlinger side: + // - Results would be able to be cached as a texture + // - There won't be texture allocation overhead, because EffectLayers don't have + // buffers + ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl(); + SurfaceControl parent = viewRootImpl != null + ? viewRootImpl.getSurfaceControl() + : null; + SurfaceControl dimLayer = new SurfaceControl.Builder() + .setName("Blur layer") + .setParent(parent) + .setOpaque(false) + .setHidden(false) + .setEffectLayer() + .build(); - depthController.setSurface(dimLayer); - backgroundRadiusAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - depthController.setIsInLaunchTransition(true); - } - - @Override - public void onAnimationEnd(Animator animation) { - depthController.setIsInLaunchTransition(false); - depthController.setSurface(null); - if (dimLayer != null) { - new SurfaceControl.Transaction() - .remove(dimLayer) - .apply(); - } - } - }); + backgroundRadiusAnim.addListener( + AnimatorListeners.forEndCallback(() -> new SurfaceControl.Transaction().remove(dimLayer).apply())); } + return backgroundRadiusAnim; } @@ -1107,30 +1103,35 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return; } if (hasControlRemoteAppTransitionPermission()) { - mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */); + RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + addRemoteAnimations(definition); + mLauncher.registerRemoteAnimations(definition); + } + } - RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); - definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, - WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, - new RemoteAnimationAdapterCompat( - new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner, - false /* startAtFrontOfQueue */), - CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */, - mLauncher.getIApplicationThread())); + /** + * Adds remote animations to a {@link RemoteAnimationDefinition}. May be + * overridden to add + * additional animations. + */ + protected void addRemoteAnimations(RemoteAnimationDefinition definition) { + mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */); + 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( - WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER, - new RemoteAnimationAdapterCompat( - new LauncherAnimationRunner( - mHandler, mKeyguardGoingAwayRunner, - true /* startAtFrontOfQueue */), - CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */, - mLauncher.getIApplicationThread())); - } - - new ActivityCompat(mLauncher).registerRemoteAnimations(definition); + 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 */)); } } @@ -1141,14 +1142,31 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } - if (!Utilities.ATLEAST_S) return; + if (!Utilities.ATLEAST_S) + return; if (hasControlRemoteAppTransitionPermission()) { mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */); - mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition( + mLauncherOpenTransition = new RemoteTransition( new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner, - false /* startAtFrontOfQueue */), mLauncher.getIApplicationThread()); - mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName()); - SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition); + false /* startAtFrontOfQueue */).toRemoteTransition(), + mLauncher.getIApplicationThread()); + + 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() }; + 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 }; + SystemUiProxy.INSTANCE.get(mLauncher) + .registerRemoteTransition(mLauncherOpenTransition, homeCheck); + } + if (mBackAnimationController != null) { + mBackAnimationController.registerBackCallbacks(mHandler); } } @@ -1156,20 +1174,20 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener unregisterRemoteAnimations(); unregisterRemoteTransitions(); mStartingWindowListener.setTransitionManager(null); - SystemUiProxy.INSTANCE.getNoCreate().setStartingWindowListener(null); + SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null); } - private void unregisterRemoteAnimations() { + protected void unregisterRemoteAnimations() { if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } if (hasControlRemoteAppTransitionPermission()) { - new ActivityCompat(mLauncher).unregisterRemoteAnimations(); + mLauncher.unregisterRemoteAnimations(); - // Also clear strong references to the runners registered with the remote animation + // Also clear strong references to the runners registered with the remote + // animation // definition so we don't have to wait for the system gc mWallpaperOpenRunner = null; - mAppLaunchRunner = null; mKeyguardGoingAwayRunner = null; } } @@ -1179,19 +1197,24 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return; } if (hasControlRemoteAppTransitionPermission()) { - if (mLauncherOpenTransition == null) return; - SystemUiProxy.INSTANCE.getNoCreate().unregisterRemoteTransition( + if (mLauncherOpenTransition == null) + return; + SystemUiProxy.INSTANCE.get(mLauncher).unregisterRemoteTransition( mLauncherOpenTransition); mLauncherOpenTransition = null; mWallpaperOpenTransitionRunner = null; } + if (mBackAnimationController != null) { + mBackAnimationController.unregisterBackCallbacks(); + mBackAnimationController = null; + } } - private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) { - for (RemoteAnimationTargetCompat target : targets) { + private boolean launcherIsATargetWithMode(RemoteAnimationTarget[] targets, int mode) { + for (RemoteAnimationTarget target : targets) { if (target.mode == mode && target.taskInfo != null - // Compare component name instead of task-id because transitions will promote - // the target up to the root task while getTaskId returns the leaf. + // Compare component name instead of task-id because transitions will promote + // the target up to the root task while getTaskId returns the leaf. && target.taskInfo.topActivity != null && target.taskInfo.topActivity.equals(mLauncher.getComponentName())) { return true; @@ -1200,45 +1223,57 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return false; } + private boolean hasMultipleTargetsWithMode(RemoteAnimationTarget[] targets, int mode) { + int numTargets = 0; + for (RemoteAnimationTarget target : targets) { + if (target.mode == mode) { + numTargets++; + } + if (numTargets > 1) { + return true; + } + } + return false; + } + /** * @return Runner that plays when user goes to Launcher - * ie. pressing home, swiping up from nav bar. + * ie. pressing home, swiping up from nav bar. */ RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) { return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock); } /** - * Animator that controls the transformations of the windows when unlocking the device. + * Animator that controls the transformations of the windows when unlocking the + * device. */ - private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets) { + 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); + float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 : QuickStepContract.getWindowCornerRadius(mLauncher); unlockAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - SurfaceParams[] params = new SurfaceParams[appTargets.length]; + SurfaceTransaction transaction = new SurfaceTransaction(); for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = appTargets[i]; - params[i] = new SurfaceParams.Builder(target.leash) - .withAlpha(1f) - .withWindowCrop(target.screenSpaceBounds) - .withCornerRadius(cornerRadius) - .build(); + RemoteAnimationTarget target = appTargets[i]; + transaction.forSurface(target.leash) + .setAlpha(1f) + .setWindowCrop(target.screenSpaceBounds) + .setCornerRadius(cornerRadius); } - surfaceApplier.scheduleApply(params); + surfaceApplier.scheduleApply(transaction); } }); return unlockAnimator; } - private static int getRotationChange(RemoteAnimationTargetCompat[] appTargets) { + private static int getRotationChange(RemoteAnimationTarget[] appTargets) { int rotationChange = 0; - for (RemoteAnimationTargetCompat target : appTargets) { + for (RemoteAnimationTarget target : appTargets) { if (Math.abs(target.rotationChange) > Math.abs(rotationChange)) { rotationChange = target.rotationChange; } @@ -1247,10 +1282,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Returns view on launcher that corresponds to the closing app in the list of app targets + * Returns view on launcher that corresponds to the closing app in the list of + * app targets */ - private @Nullable View findLauncherView(RemoteAnimationTargetCompat[] appTargets) { - for (RemoteAnimationTargetCompat appTarget : appTargets) { + private @Nullable View findLauncherView(RemoteAnimationTarget[] appTargets) { + for (RemoteAnimationTarget appTarget : appTargets) { if (appTarget.mode == MODE_CLOSING) { View launcherView = findLauncherView(appTarget); if (launcherView != null) { @@ -1264,7 +1300,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener /** * Returns view on launcher that corresponds to the {@param runningTaskTarget}. */ - private @Nullable View findLauncherView(RemoteAnimationTargetCompat runningTaskTarget) { + private @Nullable View findLauncherView(RemoteAnimationTarget runningTaskTarget) { if (runningTaskTarget == null || runningTaskTarget.taskInfo == null) { return null; } @@ -1273,7 +1309,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener runningTaskTarget.taskInfo.baseActivity, runningTaskTarget.taskInfo.origActivity, runningTaskTarget.taskInfo.realActivity, - runningTaskTarget.taskInfo.topActivity}; + runningTaskTarget.taskInfo.topActivity }; String packageName = null; for (ComponentName component : taskInfoActivities) { @@ -1287,7 +1323,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return null; } - // Find the associated item info for the launch cookie (if available), note that predicted + // 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<>() @@ -1315,24 +1352,26 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx); float secondaryDimension = orientationHandler .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx); - final float targetX = primaryDimension / 2f; + final float targetX = primaryDimension / 2f; final float targetY = secondaryDimension - dp.hotseatBarSizePx; return new RectF(targetX - halfIconSize, targetY - halfIconSize, targetX + halfIconSize, targetY + halfIconSize); } /** - * Closing animator that animates the window into its final location on the workspace. + * Closing animator that animates the window into its final location on the + * workspace. */ - private void getClosingWindowAnimators(AnimatorSet animation, - RemoteAnimationTargetCompat[] targets, View launcherView, PointF velocityPxPerS) { + private RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation, + RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS, + RectF closingWindowStartRect, float startWindowCornerRadius) { FloatingIconView floatingIconView = null; FloatingWidgetView floatingWidget = null; RectF targetRect = new RectF(); - RemoteAnimationTargetCompat runningTaskTarget = null; + RemoteAnimationTarget runningTaskTarget = null; boolean isTransluscent = false; - for (RemoteAnimationTargetCompat target : targets) { + for (RemoteAnimationTarget target : targets) { if (target.mode == MODE_CLOSING) { runningTaskTarget = target; isTransluscent = runningTaskTarget.isTranslucent; @@ -1341,25 +1380,32 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } // Get floating view and target rect. + boolean isInHotseat = false; if (launcherView instanceof LauncherAppWidgetHostView) { Size windowSize = new Size(mDeviceProfile.availableWidthPx, mDeviceProfile.availableHeightPx); - int fallbackBackgroundColor = - FloatingWidgetView.getDefaultBackgroundColor(mLauncher, runningTaskTarget); + int fallbackBackgroundColor = FloatingWidgetView.getDefaultBackgroundColor(mLauncher, runningTaskTarget); floatingWidget = FloatingWidgetView.getFloatingWidgetView(mLauncher, (LauncherAppWidgetHostView) launcherView, targetRect, windowSize, mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher), isTransluscent, fallbackBackgroundColor); } else if (launcherView != null) { - floatingIconView = getFloatingIconView(mLauncher, launcherView, + floatingIconView = getFloatingIconView(mLauncher, launcherView, null, + mLauncher.getTaskbarUIController() == null + ? null + : mLauncher.getTaskbarUIController().findMatchingView(launcherView), true /* hideOriginal */, targetRect, false /* isOpening */); + isInHotseat = launcherView.getTag() instanceof ItemInfo + && ((ItemInfo) launcherView.getTag()).isInHotseat(); } else { targetRect.set(getDefaultWindowTargetRect()); } - final RectF startRect = new RectF(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); - RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mLauncher, - mDeviceProfile); + boolean useTaskbarHotseatParams = mDeviceProfile.isTaskbarPresent && isInHotseat; + RectFSpringAnim anim = new RectFSpringAnim(useTaskbarHotseatParams + ? new TaskbarHotseatSpringConfig(mLauncher, closingWindowStartRect, targetRect) + : new DefaultSpringConfig(mLauncher, mDeviceProfile, closingWindowStartRect, + targetRect)); // Hook up floating views to the closing window animators. final int rotationChange = getRotationChange(targets); @@ -1375,11 +1421,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION; RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect, - windowTargetBounds) { + windowTargetBounds, startWindowCornerRadius) { @Override public void onUpdate(RectF currentRectF, float progress) { - finalFloatingIconView.update(1f, 255 /* fgAlpha */, currentRectF, progress, - windowAlphaThreshold, getCornerRadius(progress), false); + finalFloatingIconView.update(1f, currentRectF, progress, windowAlphaThreshold, + getCornerRadius(progress), false); super.onUpdate(currentRectF, progress); } @@ -1392,14 +1438,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final float floatingWidgetAlpha = isTransluscent ? 0 : 1; FloatingWidgetView finalFloatingWidget = floatingWidget; - RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect, - windowTargetBounds) { + RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect, + windowTargetBounds, startWindowCornerRadius) { @Override public void onUpdate(RectF currentRectF, float progress) { - final float fallbackBackgroundAlpha = - 1 - mapBoundToRange(progress, 0.8f, 1, 0, 1, EXAGGERATED_EASE); - final float foregroundAlpha = - mapBoundToRange(progress, 0.5f, 1, 0, 1, EXAGGERATED_EASE); + final float fallbackBackgroundAlpha = 1 + - mapBoundToRange(progress, 0.8f, 1, 0, 1, EXAGGERATED_EASE); + final float foregroundAlpha = mapBoundToRange(progress, 0.5f, 1, 0, 1, EXAGGERATED_EASE); finalFloatingWidget.update(currentRectF, floatingWidgetAlpha, foregroundAlpha, fallbackBackgroundAlpha, 1 - progress); @@ -1407,21 +1452,27 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }; anim.addOnUpdateListener(runner); + } else { + // If no floating icon or widget is present, animate the to the default window + // target rect. + anim.addOnUpdateListener(new SpringAnimRunner( + targets, targetRect, windowTargetBounds, startWindowCornerRadius)); } // Use a fixed velocity to start the animation. animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - anim.start(mLauncher, velocityPxPerS); + anim.start(mLauncher, mDeviceProfile, velocityPxPerS); } }); + return anim; } /** * Closing window animator that moves the window down and offscreen. */ - private Animator getFallbackClosingWindowAnimators(RemoteAnimationTargetCompat[] appTargets) { + private Animator getFallbackClosingWindowAnimators(RemoteAnimationTarget[] appTargets) { final int rotationChange = getRotationChange(appTargets); SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer); Matrix matrix = new Matrix(); @@ -1430,7 +1481,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); int duration = CLOSING_TRANSITION_DURATION_MS; float windowCornerRadius = mDeviceProfile.isMultiWindowMode - ? 0 : getWindowCornerRadius(mLauncher); + ? 0 + : getWindowCornerRadius(mLauncher); float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius; closingAnimator.setDuration(duration); closingAnimator.addUpdateListener(new MultiValueUpdateListener() { @@ -1442,10 +1494,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener @Override public void onUpdate(float percent, boolean initOnly) { - SurfaceParams[] params = new SurfaceParams[appTargets.length]; + SurfaceTransaction transaction = new SurfaceTransaction(); for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = appTargets[i]; - SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash); + RemoteAnimationTarget target = appTargets[i]; + SurfaceProperties builder = transaction.forSurface(target.leash); if (target.localBounds != null) { tmpPos.set(target.localBounds.left, target.localBounds.top); @@ -1467,20 +1519,19 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener tmpRect.centerY()); matrix.postTranslate(0, mDy.value); matrix.postTranslate(tmpPos.x, tmpPos.y); - builder.withMatrix(matrix) - .withWindowCrop(crop) - .withAlpha(mAlpha.value) - .withCornerRadius(windowCornerRadius) - .withShadowRadius(mShadowRadius.value); + builder.setMatrix(matrix) + .setWindowCrop(crop) + .setAlpha(mAlpha.value) + .setCornerRadius(windowCornerRadius) + .setShadowRadius(mShadowRadius.value); } else if (target.mode == MODE_OPENING) { matrix.setTranslate(tmpPos.x, tmpPos.y); - builder.withMatrix(matrix) - .withWindowCrop(crop) - .withAlpha(1f); + builder.setMatrix(matrix) + .setWindowCrop(crop) + .setAlpha(1f); } - params[i] = builder.build(); } - surfaceApplier.scheduleApply(params); + surfaceApplier.scheduleApply(transaction); } }); @@ -1497,9 +1548,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * Returns true if we have permission to control remote app transisions */ public boolean hasControlRemoteAppTransitionPermission() { - if (!LawnchairApp.isRecentsEnabled()) return false; - return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) - == PackageManager.PERMISSION_GRANTED; + if (!LawnchairApp.isRecentsEnabled()) + return false; + return mLauncher + .checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) == PackageManager.PERMISSION_GRANTED; } private void addCujInstrumentation(Animator anim, int cuj) { @@ -1519,9 +1571,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener InteractionJankMonitorWrapper.begin(mDragLayer, cuj); - mDragLayer.post(() -> - mDragLayer.getViewTreeObserver().removeOnDrawListener( - this)); + mDragLayer.post(() -> mDragLayer.getViewTreeObserver().removeOnDrawListener( + this)); } }); super.onAnimationStart(animation); @@ -1541,7 +1592,110 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Remote animation runner for animation from the app to Launcher, including recents. + * Creates the {@link RectFSpringAnim} and {@link AnimatorSet} required to + * animate + * the transition. + */ + public Pair createWallpaperOpenAnimations( + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + boolean fromUnlock, + RectF startRect, + float startWindowCornerRadius, + boolean fromPredictiveBack) { + AnimatorSet anim = null; + RectFSpringAnim rectFSpringAnim = null; + + RemoteAnimationProvider provider = mRemoteAnimationProvider; + if (provider != null) { + anim = provider.createWindowAnimation(appTargets, wallpaperTargets); + } + + if (anim == null) { + anim = new AnimatorSet(); + + final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible() + || launcherIsATargetWithMode(appTargets, MODE_OPENING); + + View launcherView = findLauncherView(appTargets); + boolean playFallBackAnimation = (launcherView == null + && launcherIsForceInvisibleOrOpening) + || mLauncher.getWorkspace().isOverlayShown() + || hasMultipleTargetsWithMode(appTargets, MODE_CLOSING); + + boolean playWorkspaceReveal = true; + boolean skipAllAppsScale = false; + if (fromUnlock) { + anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets)); + } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get() + && !playFallBackAnimation) { + // Use a fixed velocity to start the animation. + float velocityPxPerS = DynamicResource.provider(mLauncher) + .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s); + PointF velocity = new PointF(0, -velocityPxPerS); + rectFSpringAnim = getClosingWindowAnimators( + anim, appTargets, launcherView, velocity, startRect, + startWindowCornerRadius); + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + // Skip scaling all apps, otherwise FloatingIconView will get wrong + // layout bounds. + skipAllAppsScale = true; + } else if (!fromPredictiveBack) { + anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y, + true /* animateOverviewScrim */, launcherView).getAnimators()); + + if (!areAllTargetsTranslucent(appTargets)) { + anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth, + MULTI_PROPERTY_VALUE, + BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher))); + } + + // We play StaggeredWorkspaceAnim as a part of the closing window animation. + playWorkspaceReveal = false; + } + } else { + anim.play(getFallbackClosingWindowAnimators(appTargets)); + } + + // Normally, we run the launcher content animation when we are transitioning + // home, but if home is already visible, then we don't want to animate the + // contents of launcher unless we know that we are animating home as a result + // of the home button press with quickstep, which will result in launcher being + // started on touch down, prior to the animation home (and won't be in the + // targets list because it is already visible). In that case, we force + // invisibility on touch down, and only reset it after the animation to home + // is initialized. + if (launcherIsForceInvisibleOrOpening) { + addCujInstrumentation( + anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME); + // Only register the content animation for cancellation when state changes + mLauncher.getStateManager().setCurrentAnimation(anim); + + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + Pair contentAnimator = getLauncherContentAnimator(false, + LAUNCHER_RESUME_START_DELAY, + skipAllAppsScale); + anim.play(contentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + contentAnimator.second.run(); + } + }); + } else { + if (playWorkspaceReveal) { + anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators()); + } + } + } + } + + return new Pair(rectFSpringAnim, anim); + } + + /** + * Remote animation runner for animation from the app to Launcher, including + * recents. */ protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory { @@ -1554,10 +1708,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } @Override - public void onCreateAnimation(int transit, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, + public void onAnimationStart(int transit, + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, LauncherAnimationRunner.AnimationResult result) { if (mLauncher.isDestroyed()) { AnimatorSet anim = new AnimatorSet(); @@ -1566,97 +1720,19 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return; } - if (!mLauncher.hasBeenResumed()) { - // If launcher is not resumed, wait until new async-frame after resume - mLauncher.addOnResumeCallback(() -> - postAsyncCallback(mHandler, () -> - onCreateAnimation(transit, appTargets, wallpaperTargets, - nonAppTargets, result))); - return; - } - if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) { mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS); mLauncher.getStateManager().moveToRestState(); } - AnimatorSet anim = null; - RemoteAnimationProvider provider = mRemoteAnimationProvider; - if (provider != null) { - anim = provider.createWindowAnimation(appTargets, wallpaperTargets); - } - - if (anim == null) { - anim = new AnimatorSet(); - - final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible() - || launcherIsATargetWithMode(appTargets, MODE_OPENING); - - View launcherView = findLauncherView(appTargets); - boolean playFallBackAnimation = (launcherView == null - && launcherIsForceInvisibleOrOpening) - || mLauncher.getWorkspace().isOverlayShown(); - - boolean playWorkspaceReveal = true; - boolean skipAllAppsScale = false; - if (mFromUnlock) { - anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets)); - } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get() - && !playFallBackAnimation) { - // Use a fixed velocity to start the animation. - float velocityPxPerS = DynamicResource.provider(mLauncher) - .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s); - PointF velocity = new PointF(0, -velocityPxPerS); - getClosingWindowAnimators(anim, appTargets, launcherView, velocity); - if (!mLauncher.isInState(LauncherState.ALL_APPS)) { - anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y, - true /* animateOverviewScrim */, launcherView).getAnimators()); - // We play StaggeredWorkspaceAnim as a part of the closing window animation. - playWorkspaceReveal = false; - } else { - // Skip scaling all apps, otherwise FloatingIconView will get wrong - // layout bounds. - skipAllAppsScale = true; - } - } else { - anim.play(getFallbackClosingWindowAnimators(appTargets)); - } - - // Normally, we run the launcher content animation when we are transitioning - // home, but if home is already visible, then we don't want to animate the - // contents of launcher unless we know that we are animating home as a result - // of the home button press with quickstep, which will result in launcher being - // started on touch down, prior to the animation home (and won't be in the - // targets list because it is already visible). In that case, we force - // invisibility on touch down, and only reset it after the animation to home - // is initialized. - if (launcherIsForceInvisibleOrOpening) { - addCujInstrumentation( - anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME); - // Only register the content animation for cancellation when state changes - mLauncher.getStateManager().setCurrentAnimation(anim); - - if (mLauncher.isInState(LauncherState.ALL_APPS)) { - Pair contentAnimator = - getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY, - skipAllAppsScale); - anim.play(contentAnimator.first); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - contentAnimator.second.run(); - } - }); - } else { - if (playWorkspaceReveal) { - anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators()); - } - } - } - } + RectF windowTargetBounds = new RectF(getWindowTargetBounds(appTargets, getRotationChange(appTargets))); + Pair pair = createWallpaperOpenAnimations( + appTargets, wallpaperTargets, mFromUnlock, windowTargetBounds, + QuickStepContract.getWindowCornerRadius(mLauncher), + false /* fromPredictiveBack */); mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); - result.setAnimation(anim, mLauncher); + result.setAnimation(pair.second, mLauncher); } } @@ -1674,21 +1750,20 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } @Override - public void onCreateAnimation(int transit, - RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets, - RemoteAnimationTargetCompat[] nonAppTargets, + public void onAnimationStart(int transit, + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, LauncherAnimationRunner.AnimationResult result) { AnimatorSet anim = new AnimatorSet(); - boolean launcherClosing = - launcherIsATargetWithMode(appTargets, MODE_CLOSING); + boolean launcherClosing = launcherIsATargetWithMode(appTargets, MODE_CLOSING); final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView; final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets); final boolean skipFirstFrame; if (launchingFromWidget) { composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets, - wallpaperTargets, nonAppTargets); + wallpaperTargets, nonAppTargets, launcherClosing); addCujInstrumentation( anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET); skipFirstFrame = true; @@ -1719,6 +1794,90 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } + /** + * Remote animation runner to launch an app using System UI's animation library. + */ + private static class ContainerAnimationRunner implements RemoteAnimationFactory { + + /** The delegate runner that handles the actual animation. */ + private final RemoteAnimationDelegate mDelegate; + + private ContainerAnimationRunner( + RemoteAnimationDelegate delegate) { + mDelegate = delegate; + } + + @Nullable + private static ContainerAnimationRunner from( + View v, StartingWindowListener startingWindowListener, RunnableList onEndCallback) { + View viewToUse = findViewWithBackground(v); + if (viewToUse == null) { + viewToUse = v; + } + + // TODO(b/265134143): create a CUJ type for interaction jank monitoring. + ActivityLaunchAnimator.Controller controllerDelegate = ActivityLaunchAnimator.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. + ActivityLaunchAnimator.Controller controller = new DelegateLaunchAnimatorController(controllerDelegate) { + @Override + public boolean isBelowAnimatingWindow() { + return true; + } + }; + + ActivityLaunchAnimator.Callback callback = task -> ColorUtils.setAlphaComponent( + startingWindowListener.getBackgroundColor(), 255); + + ActivityLaunchAnimator.Listener listener = new ActivityLaunchAnimator.Listener() { + @Override + public void onLaunchAnimationEnd() { + onEndCallback.executeAllAndDestroy(); + } + }; + + return new ContainerAnimationRunner( + new ActivityLaunchAnimator.AnimationDelegate(controller, callback, listener)); + } + + /** + * Finds the closest parent of [view] (inclusive) with a background drawable. + */ + @Nullable + private static View findViewWithBackground(View view) { + View current = view; + while (current.getBackground() == null) { + if (!(current.getParent() instanceof View)) { + return null; + } + + current = (View) view.getParent(); + } + + return current; + } + + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, + LauncherAnimationRunner.AnimationResult result) { + mDelegate.onAnimationStart( + transit, appTargets, wallpaperTargets, nonAppTargets, result); + } + + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + mDelegate.onAnimationCancelled(isKeyguardOccluded); + } + } + /** * Class that holds all the variables for the app open animation. */ @@ -1745,7 +1904,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener AnimOpenProperties(Resources r, DeviceProfile dp, Rect windowTargetBounds, RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop, boolean hasSplashScreen, boolean hasDifferentAppIcon) { - // Scale the app icon to take up the entire screen. This simplifies the math when + // Scale the app icon to take up the entire screen. This simplifies the math + // when // animating the app window position / scale. float smallestSize = Math.min(windowTargetBounds.height(), windowTargetBounds.width()); float maxScaleX = smallestSize / launcherIconBounds.width(); @@ -1761,7 +1921,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener initialAppIconScale = iconStartScale; finalAppIconScale = Math.max(maxScaleX, maxScaleY); - // Animate the app icon to the center of the window bounds in screen coordinates. + // Animate the app icon to the center of the window bounds in screen + // coordinates. float centerX = windowTargetBounds.centerX() - dragLayerLeft; float centerY = windowTargetBounds.centerY() - dragLayerTop; @@ -1787,8 +1948,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - private static class StartingWindowListener extends IStartingWindowListener.Stub { + private class StartingWindowListener extends IStartingWindowListener.Stub { private QuickstepTransitionManager mTransitionManager; + private int mBackgroundColor; public void setTransitionManager(QuickstepTransitionManager transitionManager) { mTransitionManager = transitionManager; @@ -1797,6 +1959,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener @Override public void onTaskLaunching(int taskId, int supportedType, int color) { mTransitionManager.mTaskStartParams.put(taskId, Pair.create(supportedType, color)); + mBackgroundColor = color; + } + + public int getBackgroundColor() { + return mBackgroundColor == Color.TRANSPARENT + ? mLauncher.getScrimView().getBackgroundColor() + : mBackgroundColor; } } @@ -1804,7 +1973,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * RectFSpringAnim update listener to be used for app to home animation. */ private class SpringAnimRunner implements RectFSpringAnim.OnUpdateListener { - private final RemoteAnimationTargetCompat[] mAppTargets; + private final RemoteAnimationTarget[] mAppTargets; private final Matrix mMatrix = new Matrix(); private final Point mTmpPos = new Point(); private final Rect mCurrentRect = new Rect(); @@ -1815,10 +1984,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener private final Rect mTmpRect = new Rect(); - SpringAnimRunner(RemoteAnimationTargetCompat[] appTargets, RectF targetRect, - Rect windowTargetBounds) { + SpringAnimRunner(RemoteAnimationTarget[] appTargets, RectF targetRect, + Rect windowTargetBounds, float startWindowCornerRadius) { mAppTargets = appTargets; - mStartRadius = QuickStepContract.getWindowCornerRadius(mLauncher); + mStartRadius = startWindowCornerRadius; mEndRadius = Math.max(1, targetRect.width()) / 2f; mSurfaceApplier = new SurfaceTransactionApplier(mDragLayer); mWindowTargetBounds.set(windowTargetBounds); @@ -1830,10 +1999,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener @Override public void onUpdate(RectF currentRectF, float progress) { - SurfaceParams[] params = new SurfaceParams[mAppTargets.length]; + SurfaceTransaction transaction = new SurfaceTransaction(); for (int i = mAppTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = mAppTargets[i]; - SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash); + RemoteAnimationTarget target = mAppTargets[i]; + SurfaceProperties builder = transaction.forSurface(target.leash); if (target.localBounds != null) { mTmpPos.set(target.localBounds.left, target.localBounds.top); @@ -1868,18 +2037,17 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener mMatrix.setScale(scale, scale); mMatrix.postTranslate(mCurrentRect.left, mCurrentRect.top); - builder.withMatrix(mMatrix) - .withWindowCrop(mTmpRect) - .withAlpha(getWindowAlpha(progress)) - .withCornerRadius(getCornerRadius(progress) / scale); + builder.setMatrix(mMatrix) + .setWindowCrop(mTmpRect) + .setAlpha(getWindowAlpha(progress)) + .setCornerRadius(getCornerRadius(progress) / scale); } else if (target.mode == MODE_OPENING) { mMatrix.setTranslate(mTmpPos.x, mTmpPos.y); - builder.withMatrix(mMatrix) - .withAlpha(1f); + builder.setMatrix(mMatrix) + .setAlpha(1f); } - params[i] = builder.build(); } - mSurfaceApplier.scheduleApply(params); + mSurfaceApplier.scheduleApply(transaction); } protected float getWindowAlpha(float progress) { @@ -1896,4 +2064,17 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5); } } + + private static class MyDepthController extends DepthController { + MyDepthController(Launcher l) { + super(l); + setCrossWindowBlursEnabled( + CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled()); + } + + @Override + public void setSurface(SurfaceControl surface) { + super.setSurface(surface); + } + } } diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java index 1b0f967a1f..e8374b813c 100644 --- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java +++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java @@ -16,12 +16,11 @@ package com.android.launcher3.appprediction; -import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Rect; import android.graphics.Typeface; import android.os.Build; import android.text.Layout; @@ -33,24 +32,18 @@ import android.view.View; import androidx.annotation.ColorInt; import androidx.core.content.ContextCompat; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.allapps.FloatingHeaderRow; import com.android.launcher3.allapps.FloatingHeaderView; -import com.android.launcher3.statemanager.StateManager.StateListener; +import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; /** * A view which shows a horizontal divider */ @TargetApi(Build.VERSION_CODES.O) -public class AppsDividerView extends View implements StateListener, - FloatingHeaderRow { - - private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count"; - private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20; +public class AppsDividerView extends View implements FloatingHeaderRow { public enum DividerType { NONE, @@ -58,7 +51,6 @@ public class AppsDividerView extends View implements StateListener onboardingPrefs = ActivityContext.lookupContext( + getContext()).getOnboardingPrefs(); + mShowAllAppsLabel = onboardingPrefs == null || !onboardingPrefs.hasReachedMaxCount( + ALL_APPS_VISITED_COUNT); } public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { @@ -110,6 +106,14 @@ public class AppsDividerView extends View implements StateListener + extends LinearLayout implements OnDeviceProfileChangeListener, FloatingHeaderRow { - private final Launcher mLauncher; + private final T mActivityContext; private int mNumPredictedAppsPerRow; // Helper to drawing the focus indicator. @@ -64,12 +62,9 @@ public class PredictionRowView extends LinearLayout implements private final List mPredictedApps = new ArrayList<>(); private FloatingHeaderView mParent; - private boolean mScrolledOut; private boolean mPredictionsEnabled = false; - - @Nullable - private List mPendingPredictedItems; + private OnLongClickListener mOnIconLongClickListener = ItemLongClickListener.INSTANCE_ALL_APPS; public PredictionRowView(@NonNull Context context) { this(context, null); @@ -80,15 +75,21 @@ public class PredictionRowView extends LinearLayout implements setOrientation(LinearLayout.HORIZONTAL); mFocusHelper = new SimpleFocusIndicatorHelper(this); - mLauncher = Launcher.getLauncher(context); - mLauncher.addOnDeviceProfileChangeListener(this); - mNumPredictedAppsPerRow = mLauncher.getDeviceProfile().numShownAllAppsColumns; + mActivityContext = ActivityContext.lookupContext(context); + mNumPredictedAppsPerRow = mActivityContext.getDeviceProfile().numShownAllAppsColumns; updateVisibility(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + mActivityContext.addOnDeviceProfileChangeListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mActivityContext.removeOnDeviceProfileChangeListener(this); } public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { @@ -97,11 +98,11 @@ public class PredictionRowView extends LinearLayout implements private void updateVisibility() { setVisibility(mPredictionsEnabled ? VISIBLE : GONE); - if (mLauncher.getAppsView() != null) { + if (mActivityContext.getAppsView() != null) { if (mPredictionsEnabled) { - mLauncher.getAppsView().getAppsStore().registerIconContainer(this); + mActivityContext.getAppsView().getAppsStore().registerIconContainer(this); } else { - mLauncher.getAppsView().getAppsStore().unregisterIconContainer(this); + mActivityContext.getAppsView().getAppsStore().unregisterIconContainer(this); } } } @@ -120,9 +121,14 @@ public class PredictionRowView extends LinearLayout implements @Override public int getExpectedHeight() { - return getVisibility() == GONE ? 0 : - Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx - + getPaddingTop() + getPaddingBottom(); + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + int iconHeight = deviceProfile.allAppsIconSizePx; + int iconPadding = deviceProfile.allAppsIconDrawablePaddingPx; + int textHeight = Utilities.calculateTextHeight(deviceProfile.allAppsIconTextSizePx); + int verticalPadding = getResources().getDimensionPixelSize( + R.dimen.all_apps_predicted_icon_vertical_padding); + int totalHeight = iconHeight + iconPadding + textHeight + verticalPadding * 2; + return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom(); } @Override @@ -157,19 +163,10 @@ public class PredictionRowView extends LinearLayout implements * we can optimize by swapping them in place. */ public void setPredictedApps(List items) { - if (!FeatureFlags.ENABLE_APP_PREDICTIONS_WHILE_VISIBLE.get() - && !mLauncher.isWorkspaceLoading() - && isShown() - && getWindowVisibility() == View.VISIBLE) { - mPendingPredictedItems = items; - return; - } - applyPredictedApps(items); } private void applyPredictedApps(List items) { - mPendingPredictedItems = null; mPredictedApps.clear(); mPredictedApps.addAll(items.stream() .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo) @@ -177,6 +174,15 @@ public class PredictionRowView extends LinearLayout implements applyPredictionApps(); } + /** + * Sets the long click listener for predictions for any future predictions. + * + * Existing predictions in the container are not updated with this new callback. + */ + public void setOnIconLongClickListener(OnLongClickListener onIconLongClickListener) { + mOnIconLongClickListener = onIconLongClickListener; + } + @Override public void onDeviceProfileChanged(DeviceProfile dp) { mNumPredictedAppsPerRow = dp.numShownAllAppsColumns; @@ -189,18 +195,18 @@ public class PredictionRowView extends LinearLayout implements while (getChildCount() > mNumPredictedAppsPerRow) { removeViewAt(0); } - LayoutInflater inflater = mLauncher.getAppsView().getLayoutInflater(); + LayoutInflater inflater = mActivityContext.getAppsView().getLayoutInflater(); while (getChildCount() < mNumPredictedAppsPerRow) { BubbleTextView icon = (BubbleTextView) inflater.inflate( R.layout.all_apps_icon, this, false); - icon.setOnClickListener(ItemClickHandler.INSTANCE); - icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS); + icon.setOnClickListener(mActivityContext.getItemOnClickListener()); + icon.setOnLongClickListener(mOnIconLongClickListener); icon.setLongPressTimeoutFactor(1f); icon.setOnFocusChangeListener(mFocusHelper); LayoutParams lp = (LayoutParams) icon.getLayoutParams(); // Ensure the all apps icon height matches the workspace icons in portrait mode. - lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx; + lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx; lp.width = 0; lp.weight = 1; addView(icon); @@ -223,7 +229,6 @@ public class PredictionRowView extends LinearLayout implements boolean predictionsEnabled = predictionCount > 0; if (predictionsEnabled != mPredictionsEnabled) { mPredictionsEnabled = predictionsEnabled; - mLauncher.reapplyUi(false /* cancelCurrentAnimation */); updateVisibility(); } mParent.onHeightUpdated(); @@ -237,22 +242,15 @@ public class PredictionRowView extends LinearLayout implements @Override public void setVerticalScroll(int scroll, boolean isScrolledOut) { - mScrolledOut = isScrolledOut; if (!isScrolledOut) { setTranslationY(scroll); } - setAlpha(mScrolledOut ? 0 : 1); + setAlpha(isScrolledOut ? 0 : 1); if (getVisibility() != GONE) { AlphaUpdateListener.updateVisibility(this); } } - @Override - public void setInsets(Rect insets, DeviceProfile grid) { - int leftRightPadding = grid.allAppsLeftRightPadding; - setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom()); - } - @Override public Class getTypeClass() { return PredictionRowView.class; @@ -263,12 +261,4 @@ public class PredictionRowView extends LinearLayout implements return getChildAt(0); } - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - - if (mPendingPredictedItems != null && !isVisible) { - applyPredictedApps(mPendingPredictedItems); - } - } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java index 680012ce28..048243e345 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java @@ -26,22 +26,17 @@ import android.view.View; import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.Hotseat; -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.Workspace; -import com.android.launcher3.config.FeatureFlags; -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.util.GridOccupancy; import com.android.launcher3.util.IntArray; import com.android.launcher3.views.ArrowTipView; import com.android.launcher3.views.Snackbar; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.stream.IntStream; @@ -74,101 +69,12 @@ public class HotseatEduController { */ void migrate() { HotseatRestoreHelper.createBackup(mLauncher); - if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) { - migrateToFolder(); - } else { - migrateHotseatWhole(); - } + migrateHotseatWhole(); Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled, R.string.hotseat_prediction_settings, null, () -> mLauncher.startActivity(getSettingsIntent())); } - /** - * This migration places all non folder items in the hotseat into a folder and then moves - * all folders in the hotseat to a workspace page that has enough empty spots. - * - * @return pageId that has accepted the items. - */ - private int migrateToFolder() { - ArrayDeque folders = new ArrayDeque<>(); - ArrayList putIntoFolder = new ArrayList<>(); - - //separate folders and items that can get in folders - for (int i = 0; i < mLauncher.getDeviceProfile().numShownHotseatIcons; i++) { - View view = mHotseat.getChildAt(i, 0); - if (view == null) continue; - ItemInfo info = (ItemInfo) view.getTag(); - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - folders.add((FolderInfo) info); - } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings - .Favorites.CONTAINER_HOTSEAT) { - putIntoFolder.add((WorkspaceItemInfo) info); - } - } - - // create a temp folder and add non folder items to it - if (!putIntoFolder.isEmpty()) { - ItemInfo firstItem = putIntoFolder.get(0); - FolderInfo folderInfo = new FolderInfo(); - mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container, - firstItem.screenId, firstItem.cellX, firstItem.cellY); - folderInfo.setTitle("", mLauncher.getModelWriter()); - folderInfo.contents.addAll(putIntoFolder); - for (int i = 0; i < folderInfo.contents.size(); i++) { - ItemInfo item = folderInfo.contents.get(i); - item.rank = i; - mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0, - item.cellX, item.cellY); - } - folders.add(folderInfo); - } - mNewItems.addAll(folders); - - return placeFoldersInWorkspace(folders); - } - - private int placeFoldersInWorkspace(ArrayDeque folders) { - if (folders.isEmpty()) return 0; - - Workspace workspace = mLauncher.getWorkspace(); - InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv; - - GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()]; - for (int i = 0; i < occupancyList.length; i++) { - occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy(); - } - //scan every screen to find available spots to place folders - int occupancyIndex = 0; - int[] itemXY = new int[2]; - while (occupancyIndex < occupancyList.length && !folders.isEmpty()) { - GridOccupancy occupancy = occupancyList[occupancyIndex]; - if (occupancy.findVacantCell(itemXY, 1, 1)) { - FolderInfo info = folders.poll(); - mLauncher.getModelWriter().moveItemInDatabase(info, - LauncherSettings.Favorites.CONTAINER_DESKTOP, - workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]); - occupancy.markCells(info, true); - } else { - occupancyIndex++; - } - } - if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex); - int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(), - LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) - .getInt(LauncherSettings.Settings.EXTRA_VALUE); - // if all screens are full and we still have folders left, put those on a new page - FolderInfo folderInfo; - int col = 0; - while ((folderInfo = folders.poll()) != null) { - mLauncher.getModelWriter().moveItemInDatabase(folderInfo, - LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++, - idp.numRows - 1); - } - mNewScreens = IntArray.wrap(screenId); - return workspace.getPageCount(); - } - /** * This migration option attempts to move the entire hotseat up to the first workspace that * has space to host items. If no such page is found, it moves items to a new page. @@ -176,7 +82,7 @@ public class HotseatEduController { * @return pageId where items are migrated */ private int migrateHotseatWhole() { - Workspace workspace = mLauncher.getWorkspace(); + Workspace workspace = mLauncher.getWorkspace(); int pageId = -1; int toRow = 0; diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java index 119ae907f7..80bdb6f153 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java @@ -15,8 +15,7 @@ */ package com.android.launcher3.hybridhotseat; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent - .LAUNCHER_HOTSEAT_EDU_ACCEPT; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_ACCEPT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN; @@ -39,9 +38,8 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.views.AbstractSlideInView; @@ -107,19 +105,12 @@ public class HotseatEduDialog extends AbstractSlideInView implements I mDismissBtn.setOnClickListener(this::onDismiss); LinearLayout buttonContainer = findViewById(R.id.button_container); - int adjustedMarginEnd = ApiWrapper.getHotseatEndOffset(context) - - buttonContainer.getPaddingEnd(); + int adjustedMarginEnd = grid.hotseatBarEndOffset - buttonContainer.getPaddingEnd(); if (InvariantDeviceProfile.INSTANCE.get(context) .getDeviceProfile(context).isTaskbarPresent && adjustedMarginEnd > 0) { ((LinearLayout.LayoutParams) buttonContainer.getLayoutParams()).setMarginEnd( adjustedMarginEnd); } - - // update ui to reflect which migration method is going to be used - if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) { - ((TextView) findViewById(R.id.hotseat_edu_content)).setText( - R.string.hotseat_edu_message_migrate_alt); - } } private void onAccept(View v) { @@ -202,7 +193,7 @@ public class HotseatEduDialog extends AbstractSlideInView implements I icon.setEnabled(false); icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); icon.verifyHighRes(); - CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1); + CellLayoutLayoutParams lp = new CellLayoutLayoutParams(i, 0, 1, 1); mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true); } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index 85d9f01735..85d0ab5315 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -33,6 +33,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; @@ -41,7 +42,6 @@ import com.android.launcher3.Hotseat; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.anim.AnimationSuccessListener; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.graphics.DragPreviewProvider; @@ -52,16 +52,18 @@ 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.popup.SystemShortcut; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.ItemLongClickListener; import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.views.Snackbar; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -90,10 +92,14 @@ public class HotseatPredictionController implements DragController.DragListener, private List mOutlineDrawings = new ArrayList<>(); + private boolean mEnableHotseatLongPressTipForTesting = true; + private final View.OnLongClickListener mPredictionLongClickListener = v -> { if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; if (mLauncher.getWorkspace().isSwitchingState()) return false; - if (!mLauncher.getOnboardingPrefs().getBoolean( + + TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onWorkspaceItemLongClick"); + if (mEnableHotseatLongPressTipForTesting && !mLauncher.getOnboardingPrefs().getBoolean( OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) { Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, R.string.hotseat_prediction_settings, null, @@ -133,6 +139,12 @@ public class HotseatPredictionController implements DragController.DragListener, onHotseatHierarchyChanged(); } + /** Enables/disabled the hotseat prediction icon long press edu for testing. */ + @VisibleForTesting + public void enableHotseatEdu(boolean enable) { + mEnableHotseatLongPressTipForTesting = enable; + } + private void onHotseatHierarchyChanged() { if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) { // Post update after a single frame to avoid layout within layout @@ -279,33 +291,7 @@ public class HotseatPredictionController implements DragController.DragListener, * Sets or updates the predicted items */ public void setPredictedItems(FixedContainerItems items) { - boolean shouldIgnoreVisibility = FeatureFlags.ENABLE_APP_PREDICTIONS_WHILE_VISIBLE.get() - || mLauncher.isWorkspaceLoading() - || mPredictedItems.equals(items.items) - || mHotseat.getShortcutsAndWidgets().getChildCount() < mHotSeatItemsCount; - if (!shouldIgnoreVisibility - && mHotseat.isShown() - && mHotseat.getWindowVisibility() == View.VISIBLE) { - mHotseat.setOnVisibilityAggregatedCallback((isVisible) -> { - if (isVisible) { - return; - } - mHotseat.setOnVisibilityAggregatedCallback(null); - - applyPredictedItems(items); - }); - } else { - mHotseat.setOnVisibilityAggregatedCallback(null); - - applyPredictedItems(items); - } - } - - /** - * Sets or updates the predicted items only once the hotseat becomes hidden to the user - */ - private void applyPredictedItems(FixedContainerItems items) { - mPredictedItems = items.items; + mPredictedItems = new ArrayList(items.items); if (mPredictedItems.isEmpty()) { HotseatRestoreHelper.restoreBackup(mLauncher); } @@ -409,11 +395,11 @@ public class HotseatPredictionController implements DragController.DragListener, @Nullable @Override public SystemShortcut getShortcut(QuickstepLauncher activity, - ItemInfo itemInfo) { + ItemInfo itemInfo, View originalView) { if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { return null; } - return new PinPrediction(activity, itemInfo); + return new PinPrediction(activity, itemInfo, originalView); } private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) { @@ -480,8 +466,8 @@ public class HotseatPredictionController implements DragController.DragListener, * * @param matcher filter matching items that have been removed */ - public void onModelItemsRemoved(ItemInfoMatcher matcher) { - if (mPredictedItems.removeIf(matcher::matchesInfo)) { + public void onModelItemsRemoved(Predicate matcher) { + if (mPredictedItems.removeIf(matcher)) { fillGapsWithPrediction(true); } } @@ -498,9 +484,9 @@ public class HotseatPredictionController implements DragController.DragListener, private class PinPrediction extends SystemShortcut { - private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) { + private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo, View originalView) { super(R.drawable.ic_pin, R.string.pin_prediction, target, - itemInfo); + itemInfo, originalView); } @Override diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java index 7c29c5b4c6..0dde1bd943 100644 --- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java +++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java @@ -18,12 +18,15 @@ package com.android.launcher3.model; import static android.app.prediction.AppTargetEvent.ACTION_DISMISS; import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH; import static android.app.prediction.AppTargetEvent.ACTION_PIN; +import static android.app.prediction.AppTargetEvent.ACTION_UNDISMISS; import static android.app.prediction.AppTargetEvent.ACTION_UNPIN; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED; @@ -59,6 +62,7 @@ import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.android.launcher3.Utilities; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; import com.android.launcher3.logger.LauncherAtom.FolderContainer; @@ -113,7 +117,8 @@ public class AppEventProducer implements StatsLogConsumer { @AnyThread private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId, int targetPredictor) { - if (target != null) { + // TODO: remove the running test check when b/231648228 is fixed. + if (target != null && !Utilities.isRunningInTestHarness()) { AppTargetEvent event = new AppTargetEvent.Builder(target, eventId) .setLaunchLocation(getContainer(locationInfo)) .build(); @@ -137,12 +142,12 @@ public class AppEventProducer implements StatsLogConsumer { if (mLastDragItem == null) { return; } - if (isTrackedForHotseatPrediction(atomInfo)) { - sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION); - } if (isTrackedForHotseatPrediction(mLastDragItem)) { sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION); } + if (isTrackedForHotseatPrediction(atomInfo)) { + sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION); + } if (isTrackedForWidgetPrediction(atomInfo)) { sendEvent(atomInfo, ACTION_PIN, CONTAINER_WIDGETS_PREDICTION); } @@ -175,6 +180,8 @@ public class AppEventProducer implements StatsLogConsumer { mContext.getPackageName(), Process.myUserHandle()) .build(); sendEvent(target, atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION); + } else if (event == LAUNCHER_DISMISS_PREDICTION_UNDO) { + sendEvent(atomInfo, ACTION_UNDISMISS, CONTAINER_HOTSEAT_PREDICTION); } } @@ -293,10 +300,9 @@ public class AppEventProducer implements StatsLogConsumer { case SEARCH_RESULT_CONTAINER: return "search-results"; case EXTENDED_CONTAINERS: { - switch(ci.getExtendedContainers().getContainerCase()) { - case DEVICE_SEARCH_RESULT_CONTAINER: - case CORRECTED_DEVICE_SEARCH_RESULT_CONTAINER: - return "search-results"; + if (ci.getExtendedContainers().getContainerCase() + == DEVICE_SEARCH_RESULT_CONTAINER) { + return "search-results"; } } default: // fall out diff --git a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java index b0fba3d5ae..e504141024 100644 --- a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java @@ -27,14 +27,17 @@ import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.os.UserHandle; +import androidx.annotation.NonNull; + import com.android.launcher3.LauncherAppState; -import com.android.launcher3.Utilities; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import java.util.HashSet; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -53,20 +56,21 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { } @Override - public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { + public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, + @NonNull final AllAppsList apps) { Context context = app.getContext(); // TODO: remove this - Utilities.getDevicePrefs(context).edit() + LauncherPrefs.getDevicePrefs(context).edit() .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply(); - FixedContainerItems fci = mPredictorState.items; - Set usersForChangedShortcuts = new HashSet<>(fci.items.stream() - .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT) - .map(info -> info.user) - .collect(Collectors.toSet())); - fci.items.clear(); + Set usersForChangedShortcuts = + dataModel.extraItems.get(mPredictorState.containerId).items.stream() + .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT) + .map(info -> info.user) + .collect(Collectors.toSet()); + List items = new ArrayList<>(mTargets.size()); for (AppTarget target : mTargets) { WorkspaceItemInfo itemInfo; ShortcutInfo si = target.getShortcutInfo(); @@ -86,7 +90,7 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { .filter(info -> user.equals(info.user) && cn.equals(info.componentName)) .map(ai -> { app.getIconCache().getTitleAndIcon(ai, false); - return ai.makeWorkspaceItem(); + return ai.makeWorkspaceItem(context); }) .findAny() .orElseGet(() -> { @@ -97,7 +101,7 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { } AppInfo ai = new AppInfo(context, lai, user); app.getIconCache().getTitleAndIcon(ai, lai, false); - return ai.makeWorkspaceItem(); + return ai.makeWorkspaceItem(context); }); if (itemInfo == null) { @@ -105,10 +109,12 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { } } - itemInfo.container = fci.containerId; - fci.items.add(itemInfo); + itemInfo.container = mPredictorState.containerId; + items.add(itemInfo); } + FixedContainerItems fci = new FixedContainerItems(mPredictorState.containerId, items); + dataModel.extraItems.put(fci.containerId, fci); bindExtraContainerItems(fci); usersForChangedShortcuts.forEach( u -> dataModel.updateShortcutPinnedState(app.getContext(), u)); diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 0d7b588d35..5849b51735 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -18,12 +18,12 @@ package com.android.launcher3.model; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.formatElapsedTime; +import static com.android.launcher3.LauncherPrefs.getDevicePrefs; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; -import static com.android.launcher3.Utilities.getDevicePrefs; import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle; import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo; import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation; @@ -119,25 +119,46 @@ public class QuickstepModelDelegate extends ModelDelegate { } @Override - @WorkerThread - public void loadItems(UserManagerState ums, Map pinnedShortcuts) { + public void loadHotseatItems(UserManagerState ums, + Map pinnedShortcuts) { // TODO: Implement caching and preloading - super.loadItems(ums, pinnedShortcuts); + super.loadHotseatItems(ums, pinnedShortcuts); - WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory( - mApp, ums, pinnedShortcuts, mIDP.numDatabaseAllAppsColumns); - mAllAppsState.items.setItems( - mAllAppsState.storage.read(mApp.getContext(), allAppsFactory, ums.allUsers::get)); - mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items); - - WorkspaceItemFactory hotseatFactory = - new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numDatabaseHotseatIcons); - mHotseatState.items.setItems( + WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, + mIDP.numDatabaseHotseatIcons, mHotseatState.containerId); + FixedContainerItems hotseatItems = new FixedContainerItems(mHotseatState.containerId, mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get)); - mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items); + mDataModel.extraItems.put(mHotseatState.containerId, hotseatItems); + } + + @Override + public void loadAllAppsItems(UserManagerState ums, + Map pinnedShortcuts) { + // TODO: Implement caching and preloading + super.loadAllAppsItems(ums, pinnedShortcuts); + + WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, + mIDP.numDatabaseAllAppsColumns, mAllAppsState.containerId); + FixedContainerItems allAppsPredictionItems = new FixedContainerItems( + mAllAppsState.containerId, mAllAppsState.storage.read(mApp.getContext(), + allAppsFactory, ums.allUsers::get)); + mDataModel.extraItems.put(mAllAppsState.containerId, allAppsPredictionItems); + } + + @Override + public void loadWidgetsRecommendationItems() { + // TODO: Implement caching and preloading + super.loadWidgetsRecommendationItems(); // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. - mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items); + mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, + new FixedContainerItems(mWidgetsRecommendationState.containerId, + new ArrayList<>())); + } + + @Override + public void markActive() { + super.markActive(); mActive = true; } @@ -240,9 +261,9 @@ public class QuickstepModelDelegate extends ModelDelegate { "Item info: %s found with invalid container: %s", info, containerInfo)); - } else { - return (FolderInfo) containerInfo; } + // Allow crash to help debug b/173838775 + return (FolderInfo) containerInfo; } return null; } @@ -313,6 +334,7 @@ public class QuickstepModelDelegate extends ModelDelegate { } private void registerPredictor(PredictorState state, AppPredictor predictor) { + state.setTargets(Collections.emptyList()); state.predictor = predictor; state.predictor.registerPredictionUpdates( MODEL_EXECUTOR, new AppPredictor.Callback() { @@ -391,14 +413,14 @@ public class QuickstepModelDelegate extends ModelDelegate { static class PredictorState { - public final FixedContainerItems items; + public final int containerId; public final PersistedItemArray storage; public AppPredictor predictor; private List mLastTargets; - PredictorState(int container, String storageName) { - items = new FixedContainerItems(container); + PredictorState(int containerId, String storageName) { + this.containerId = containerId; storage = new PersistedItemArray<>(storageName); mLastTargets = Collections.emptyList(); } @@ -451,15 +473,17 @@ public class QuickstepModelDelegate extends ModelDelegate { private final UserManagerState mUMS; private final Map mPinnedShortcuts; private final int mMaxCount; + private final int mContainer; private int mReadCount = 0; protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, - Map pinnedShortcuts, int maxCount) { + Map pinnedShortcuts, int maxCount, int container) { mAppState = appState; mUMS = ums; mPinnedShortcuts = pinnedShortcuts; mMaxCount = maxCount; + mContainer = container; } @Nullable @@ -477,9 +501,10 @@ public class QuickstepModelDelegate extends ModelDelegate { return null; } AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user)); + info.container = mContainer; mAppState.getIconCache().getTitleAndIcon(info, lai, false); mReadCount++; - return info.makeWorkspaceItem(); + return info.makeWorkspaceItem(mAppState.getContext()); } case ITEM_TYPE_DEEP_SHORTCUT: { ShortcutKey key = ShortcutKey.fromIntent(intent, user); @@ -491,6 +516,7 @@ public class QuickstepModelDelegate extends ModelDelegate { return null; } WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext()); + wii.container = mContainer; mAppState.getIconCache().getShortcutIcon(wii, si); mReadCount++; return wii; diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java index e489cb3a71..d8fd51a850 100644 --- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java +++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java @@ -18,7 +18,7 @@ package com.android.launcher3.model; import static android.content.ContentResolver.SCHEME_CONTENT; -import static com.android.launcher3.Utilities.newContentObserver; +import static com.android.launcher3.util.SimpleBroadcastReceiver.getPackageFilter; import android.annotation.TargetApi; import android.app.RemoteAction; @@ -40,6 +40,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; +import android.view.View; import androidx.annotation.MainThread; import androidx.annotation.Nullable; @@ -56,7 +57,6 @@ import com.android.launcher3.popup.RemoteActionShortcut; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.util.BgObjectWithLooper; import com.android.launcher3.util.MainThreadInitializedObject; -import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SimpleBroadcastReceiver; @@ -118,7 +118,7 @@ public final class WellbeingModel extends BgObjectWithLooper { if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { mContext.registerReceiver( new SimpleBroadcastReceiver(t -> restartObserver()), - PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg, + getPackageFilter(mWellbeingProviderPkg, Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED, Intent.ACTION_PACKAGE_RESTARTED), @@ -193,7 +193,7 @@ public final class WellbeingModel extends BgObjectWithLooper { @MainThread private SystemShortcut getShortcutForApp(String packageName, int userId, - BaseDraggingActivity activity, ItemInfo info) { + BaseDraggingActivity activity, ItemInfo info, View originalView) { Preconditions.assertUIThread(); // Work profile apps are not recognized by digital wellbeing. if (userId != UserHandle.myUserId()) { @@ -217,7 +217,7 @@ public final class WellbeingModel extends BgObjectWithLooper { "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle() + "'"); } - return new RemoteActionShortcut(action, activity, info); + return new RemoteActionShortcut(action, activity, info, originalView); } } @@ -378,8 +378,8 @@ public final class WellbeingModel extends BgObjectWithLooper { * Shortcut factory for generating wellbeing action */ public static final SystemShortcut.Factory SHORTCUT_FACTORY = - (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity) - .getShortcutForApp( + (activity, info, originalView) -> (info.getTargetComponent() == null) ? null + : INSTANCE.get(activity).getShortcutForApp( info.getTargetComponent().getPackageName(), info.user.getIdentifier(), - activity, info); + activity, info, originalView); } diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index 4be83dc0b0..6160378767 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -18,20 +18,23 @@ package com.android.launcher3.model; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import android.app.prediction.AppTarget; -import android.content.ComponentName; import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.android.launcher3.LauncherAppState; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; +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 java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; /** Task to update model as a result of predicted widgets update */ @@ -52,54 +55,51 @@ public final class WidgetsPredictionUpdateTask extends BaseModelUpdateTask { * workspace. */ @Override - public void execute(LauncherAppState appState, BgDataModel dataModel, AllAppsList apps) { + public void execute(@NonNull final LauncherAppState appState, + @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { Set widgetsInWorkspace = dataModel.appWidgets.stream().map( widget -> new ComponentKey(widget.providerName, widget.user)).collect( Collectors.toSet()); + Predicate notOnWorkspace = w -> !widgetsInWorkspace.contains(w); Map> allWidgets = dataModel.widgetsModel.getAllWidgetsWithoutShortcuts(); - FixedContainerItems fixedContainerItems = mPredictorState.items; - fixedContainerItems.items.clear(); + List servicePredictedItems = new ArrayList<>(); + List localFilteredWidgets = new ArrayList<>(); - if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) { - for (AppTarget app : mTargets) { - PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), - app.getUser()); - if (allWidgets.containsKey(packageUserKey)) { - List notAddedWidgets = allWidgets.get(packageUserKey).stream() - .filter(item -> - !widgetsInWorkspace.contains( - new ComponentKey(item.componentName, item.user))) - .collect(Collectors.toList()); - if (notAddedWidgets.size() > 0) { - // Even an apps have more than one widgets, we only include one widget. - fixedContainerItems.items.add( - new PendingAddWidgetInfo( - notAddedWidgets.get(0).widgetInfo, - CONTAINER_WIDGETS_PREDICTION)); - } - } + for (AppTarget app : mTargets) { + PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser()); + List widgets = allWidgets.get(packageUserKey); + if (widgets == null || widgets.isEmpty()) { + continue; } - } else { - Map widgetItems = - allWidgets.values().stream().flatMap(List::stream).distinct() - .collect(Collectors.toMap(widget -> (ComponentKey) widget, - widget -> widget)); - for (AppTarget app : mTargets) { - if (TextUtils.isEmpty(app.getClassName())) { + String className = app.getClassName(); + if (!TextUtils.isEmpty(className)) { + WidgetItem item = widgets.stream() + .filter(w -> className.equals(w.componentName.getClassName())) + .filter(notOnWorkspace) + .findFirst() + .orElse(null); + if (item != null) { + servicePredictedItems.add(item); continue; } - ComponentKey targetWidget = new ComponentKey( - new ComponentName(app.getPackageName(), app.getClassName()), app.getUser()); - if (widgetItems.containsKey(targetWidget)) { - fixedContainerItems.items.add( - new PendingAddWidgetInfo(widgetItems.get( - targetWidget).widgetInfo, - CONTAINER_WIDGETS_PREDICTION)); - } } + // No widget was added by the service, try local filtering + widgets.stream().filter(notOnWorkspace).findFirst() + .ifPresent(localFilteredWidgets::add); } + if (servicePredictedItems.isEmpty()) { + servicePredictedItems.addAll(localFilteredWidgets); + } + + List items = servicePredictedItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION)) + .collect(Collectors.toList()); + FixedContainerItems fixedContainerItems = + new FixedContainerItems(mPredictorState.containerId, items); + + dataModel.extraItems.put(mPredictorState.containerId, fixedContainerItems); bindExtraContainerItems(fixedContainerItems); // Don't store widgets prediction to disk because it is not used frequently. diff --git a/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java b/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java new file mode 100644 index 0000000000..184ea717cd --- /dev/null +++ b/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java @@ -0,0 +1,45 @@ +/* + * 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.popup; + +import android.view.View; + +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.splitscreen.SplitShortcut; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; + +/** {@link SystemShortcut.Factory} implementation to create workspace split shortcuts */ +public interface QuickstepSystemShortcut { + + String TAG = QuickstepSystemShortcut.class.getSimpleName(); + + static SystemShortcut.Factory getSplitSelectShortcutByPosition( + SplitPositionOption position) { + return (activity, itemInfo, originalView) -> + new QuickstepSystemShortcut.SplitSelectSystemShortcut(activity, itemInfo, + originalView, position); + } + + class SplitSelectSystemShortcut extends SplitShortcut { + + public SplitSelectSystemShortcut(QuickstepLauncher launcher, ItemInfo itemInfo, + View originalView, SplitPositionOption position) { + super(position.iconResId, position.textResId, launcher, itemInfo, originalView, + position); + } + } +} diff --git a/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java b/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java index bee8bb8132..b47ef47423 100644 --- a/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java +++ b/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java @@ -16,6 +16,10 @@ package com.android.launcher3.proxy; +import static android.app.PendingIntent.FLAG_MUTABLE; +import static android.app.PendingIntent.FLAG_ONE_SHOT; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; + import android.app.Activity; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; @@ -45,7 +49,7 @@ public class StartActivityParams implements Parcelable { public StartActivityParams(Activity activity, int requestCode) { this(activity.createPendingResult(requestCode, new Intent(), - PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT), requestCode); + FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_MUTABLE), requestCode); } public StartActivityParams(PendingIntent pendingIntent, int requestCode) { diff --git a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java new file mode 100644 index 0000000000..8720bd8f26 --- /dev/null +++ b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java @@ -0,0 +1,67 @@ +/* + * 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.secondarydisplay; + +import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT; + +import android.content.Context; +import android.view.View; + +import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.appprediction.AppsDividerView; +import com.android.launcher3.appprediction.PredictionRowView; +import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.util.OnboardingPrefs; +import com.android.launcher3.views.ActivityContext; + +/** + * Implementation of SecondaryDisplayPredictions. + */ +@SuppressWarnings("unused") +public final class SecondaryDisplayPredictionsImpl extends SecondaryDisplayPredictions { + private final ActivityContext mActivityContext; + + public SecondaryDisplayPredictionsImpl(Context context) { + mActivityContext = ActivityContext.lookupContext(context); + } + + @Override + void updateAppDivider() { + OnboardingPrefs onboardingPrefs = mActivityContext.getOnboardingPrefs(); + if (onboardingPrefs != null) { + mActivityContext.getAppsView().getFloatingHeaderView() + .findFixedRowByType(AppsDividerView.class) + .setShowAllAppsLabel( + !onboardingPrefs.hasReachedMaxCount(ALL_APPS_VISITED_COUNT)); + onboardingPrefs.incrementEventCount(ALL_APPS_VISITED_COUNT); + } + } + + @Override + public void setPredictedApps(BgDataModel.FixedContainerItems item) { + mActivityContext.getAppsView().getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class) + .setPredictedApps(item.items); + } + + @Override + public void setLongClickListener(ActivityAllAppsContainerView appsView, + View.OnLongClickListener onIconLongClickListener) { + appsView.getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class) + .setOnIconLongClickListener(onIconLongClickListener); + } +} diff --git a/quickstep/src/com/android/launcher3/splitscreen/SplitShortcut.kt b/quickstep/src/com/android/launcher3/splitscreen/SplitShortcut.kt new file mode 100644 index 0000000000..2b6f77f231 --- /dev/null +++ b/quickstep/src/com/android/launcher3/splitscreen/SplitShortcut.kt @@ -0,0 +1,93 @@ +/* + * 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.splitscreen + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.util.Log +import android.view.View +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.popup.QuickstepSystemShortcut +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource +import com.android.launcher3.views.ActivityContext + +/** + * Shortcut to allow starting split. Default interaction for [onClick] is to launch split selection + * mode + */ +abstract class SplitShortcut( + iconResId: Int, + labelResId: Int, + target: T, + itemInfo: ItemInfo?, + originalView: View?, + protected val position: SplitPositionOption +) : SystemShortcut(iconResId, labelResId, target, itemInfo, originalView) where +T : Context?, +T : ActivityContext? { + private val TAG = SystemShortcut::class.java.simpleName + + // Initiate splitscreen from the Home screen or Home All Apps + protected val splitSelectSource: SplitSelectSource? + get() { + // Initiate splitscreen from the Home screen or Home All Apps + val bitmap: Bitmap + val intent: Intent + when (mItemInfo) { + is WorkspaceItemInfo -> { + val workspaceItemInfo = mItemInfo + bitmap = workspaceItemInfo.bitmap.icon + intent = workspaceItemInfo.intent + } + is com.android.launcher3.model.data.AppInfo -> { + val appInfo = mItemInfo + bitmap = appInfo.bitmap.icon + intent = appInfo.intent + } + else -> { + Log.e(TAG, "unknown item type") + return null + } + } + val splitEvent = + SplitConfigurationOptions.getLogEventForPosition(position.stagePosition) + return SplitSelectSource( + mOriginalView, + BitmapDrawable(bitmap), + intent, + position, + mItemInfo, + splitEvent + ) + } + + /** Starts split selection on the provided [mTarget] */ + override fun onClick(view: View?) { + val splitSelectSource = splitSelectSource + if (splitSelectSource == null) { + Log.w(QuickstepSystemShortcut.TAG, "no split selection source") + return + } + mTarget!!.startSplitSelection(splitSelectSource) + } +} diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java deleted file mode 100644 index 1f268cc9d9..0000000000 --- a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java +++ /dev/null @@ -1,64 +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.statehandlers; - -import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.quickstep.AnimatedFloat.VALUE; -import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS; - -import com.android.launcher3.BaseQuickstepLauncher; -import com.android.launcher3.LauncherState; -import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.statemanager.StateManager.StateHandler; -import com.android.launcher3.states.StateAnimationConfig; -import com.android.launcher3.util.UiThreadHelper; -import com.android.quickstep.AnimatedFloat; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SystemUiProxy; - -/** - * State handler for animating back button alpha in two-button nav mode. - */ -public class BackButtonAlphaHandler implements StateHandler { - - private final BaseQuickstepLauncher mLauncher; - private final AnimatedFloat mBackAlpha = new AnimatedFloat(this::updateBackAlpha); - - public BackButtonAlphaHandler(BaseQuickstepLauncher launcher) { - mLauncher = launcher; - } - - @Override - public void setState(LauncherState state) { } - - @Override - public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, - PendingAnimation animation) { - if (SysUINavigationMode.getMode(mLauncher) != TWO_BUTTONS) { - return; - } - - mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastNavButtonAlpha(); - animation.setFloat(mBackAlpha, VALUE, - mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR); - } - - private void updateBackAlpha() { - UiThreadHelper.setBackButtonAlphaAsync(mLauncher, - BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, mBackAlpha.value, false /* animate */); - } -} diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 9da2134999..c3f7eda1c3 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -19,39 +19,23 @@ package com.android.launcher3.statehandlers; import static com.android.launcher3.anim.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.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; -import android.os.IBinder; -import android.os.SystemProperties; -import android.util.FloatProperty; -import android.view.AttachedSurfaceControl; import android.view.CrossWindowBlurListeners; -import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; -import androidx.datastore.preferences.core.Preferences; - import com.android.launcher3.BaseActivity; 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.PendingAnimation; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; -import com.android.systemui.shared.system.BlurUtils; -import com.android.systemui.shared.system.WallpaperManagerCompat; -import com.patrykmichalik.opto.domain.Preference; -import com.patrykmichalik.opto.core.PreferenceExtensionsKt; - -import app.lawnchair.LawnchairApp; -import app.lawnchair.preferences.BasePreferenceManager; -import app.lawnchair.preferences.PreferenceManager; -import app.lawnchair.preferences2.PreferenceManager2; +import com.android.quickstep.util.BaseDepthController; import java.io.PrintWriter; import java.util.function.Consumer; @@ -59,181 +43,62 @@ import java.util.function.Consumer; /** * Controls blur and wallpaper zoom, for the Launcher surface only. */ -public class DepthController implements StateHandler, +public class DepthController extends BaseDepthController implements StateHandler, BaseActivity.MultiWindowModeChangedListener { - public static final FloatProperty DEPTH = - new FloatProperty("depth") { - @Override - public void setValue(DepthController depthController, float depth) { - depthController.setDepth(depth); - } + private final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; - @Override - public Float get(DepthController depthController) { - return depthController.mDepth; - } - }; + private final Consumer mCrossWindowBlurListener = this::setCrossWindowBlursEnabled; - /** - * A property that updates the background blur within a given range of values (ie. even if the - * animator goes beyond 0..1, the interpolated value will still be bounded). - */ - public static class ClampedDepthProperty extends FloatProperty { - private final float mMinValue; - private final float mMaxValue; - - public ClampedDepthProperty(float minValue, float maxValue) { - super("depthClamped"); - mMinValue = minValue; - mMaxValue = maxValue; - } - - @Override - public void setValue(DepthController depthController, float depth) { - depthController.setDepth(Utilities.boundToRange(depth, mMinValue, mMaxValue)); - } - - @Override - public Float get(DepthController depthController) { - return depthController.mDepth; - } - } - - private final ViewTreeObserver.OnDrawListener mOnDrawListener = - new ViewTreeObserver.OnDrawListener() { - @Override - public void onDraw() { - View view = mLauncher.getDragLayer(); - ViewRootImpl viewRootImpl = view.getViewRootImpl(); - boolean applied = setSurface( - viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); - if (!applied) { - dispatchTransactionSurface(mDepth); - } - view.post(() -> view.getViewTreeObserver().removeOnDrawListener(this)); - } - }; - - private final Consumer mCrossWindowBlurListener = new Consumer() { - @Override - public void accept(Boolean enabled) { - mCrossWindowBlursEnabled = enabled; - dispatchTransactionSurface(mDepth); - } - }; - - private final Runnable mOpaquenessListener = new Runnable() { - @Override - public void run() { - dispatchTransactionSurface(mDepth); - } - }; - - private final Launcher mLauncher; - /** - * Blur radius when completely zoomed out, in pixels. - */ - private int mMaxBlurRadius; - private boolean mCrossWindowBlursEnabled; - private WallpaperManagerCompat mWallpaperManager; - private SurfaceControl mSurface; - /** - * How visible the -1 overlay is, from 0 to 1. - */ - private float mOverlayScrollProgress; - /** - * Ratio from 0 to 1, where 0 is fully zoomed out, and 1 is zoomed in. - * @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float) - */ - private float mDepth; - /** - * Last blur value, in pixels, that was applied. - * For debugging purposes. - */ - private int mCurrentBlur; - /** - * If we're launching and app and should not be blurring the screen for performance reasons. - */ - private boolean mBlurDisabledForAppLaunch; - /** - * If we requested early wake-up offsets to SurfaceFlinger. - */ - private boolean mInEarlyWakeUp; + private final Runnable mOpaquenessListener = this::applyDepthAndBlur; // Workaround for animating the depth when multiwindow mode changes. private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false; - // Hints that there is potentially content behind Launcher and that we shouldn't optimize by - // marking the launcher surface as opaque. Only used in certain Launcher states. - private boolean mHasContentBehindLauncher; - private View.OnAttachStateChangeListener mOnAttachListener; - private BasePreferenceManager.FloatPref mDrawerOpacity; - private final boolean mEnableDepth; - public DepthController(Launcher l) { - mLauncher = l; - // TODO: No need to use app context here after merging Android 13 branch. - // https://cs.android.com/android/platform/superproject/+/master:packages/apps/Launcher3/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java;drc=eef0b1640bbc8c26c824836271a71b929726e895;l=175 - Preference depthPref = PreferenceManager2.getInstance(LawnchairApp.getInstance()).getWallpaperDepthEffect(); - mEnableDepth = PreferenceExtensionsKt.firstBlocking(depthPref); + super(l); + } + + private void onLauncherDraw() { + View view = mLauncher.getDragLayer(); + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); + view.post(() -> view.getViewTreeObserver().removeOnDrawListener(mOnDrawListener)); } private void ensureDependencies() { - if (mWallpaperManager == null) { - mMaxBlurRadius = mLauncher.getResources().getInteger(R.integer.max_depth_blur_radius); - mWallpaperManager = new WallpaperManagerCompat(mLauncher); - } - - if (Utilities.ATLEAST_R && mLauncher.getRootView() != null && mOnAttachListener == null) { + if (mLauncher.getRootView() != null && mOnAttachListener == null) { + View rootView = mLauncher.getRootView(); mOnAttachListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { + CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), + mCrossWindowBlurListener); + mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); + // To handle the case where window token is invalid during last setDepth call. - IBinder windowToken = mLauncher.getRootView().getWindowToken(); - if (windowToken != null) { - mWallpaperManager.setWallpaperZoomOut(windowToken, mEnableDepth ? mDepth : 1f); - } - onAttached(); + applyDepthAndBlur(); } @Override public void onViewDetachedFromWindow(View view) { - if (Utilities.ATLEAST_S) { - CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); - } + CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); } }; - mLauncher.getRootView().addOnAttachStateChangeListener(mOnAttachListener); - if (mLauncher.getRootView().isAttachedToWindow()) { - onAttached(); + rootView.addOnAttachStateChangeListener(mOnAttachListener); + if (rootView.isAttachedToWindow()) { + mOnAttachListener.onViewAttachedToWindow(rootView); } } - if (mDrawerOpacity == null) { - mDrawerOpacity = PreferenceManager.getInstance(mLauncher).getDrawerOpacity(); - } - } - - private void onAttached() { - if (Utilities.ATLEAST_S) { - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); - } - mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); - } - - public void setHasContentBehindLauncher(boolean hasContentBehindLauncher) { - mHasContentBehindLauncher = hasContentBehindLauncher; } /** * Sets if the underlying activity is started or not */ public void setActivityStarted(boolean isStarted) { - if (!Utilities.ATLEAST_R) return; if (isStarted) { mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); } else { @@ -242,152 +107,43 @@ public class DepthController implements StateHandler, } } - /** - * Sets the specified app target surface to apply the blur to. - * @return true when surface was valid and transaction was dispatched. - */ - public boolean setSurface(SurfaceControl surface) { - // Set launcher as the SurfaceControl when we don't need an external target anymore. - if (surface == null) { - ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl(); - surface = viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null; - } - if (mSurface != surface) { - mSurface = surface; - if (surface != null) { - dispatchTransactionSurface(mDepth); - return true; - } - } - return false; - } - @Override public void setState(LauncherState toState) { - if (mSurface == null || mIgnoreStateChangesDuringMultiWindowAnimation) { + if (mIgnoreStateChangesDuringMultiWindowAnimation) { return; } - float toDepth = toState.getDepth(mLauncher); - if (Float.compare(mDepth, toDepth) != 0) { - setDepth(toDepth); - } else if (toState == LauncherState.OVERVIEW) { - dispatchTransactionSurface(mDepth); - } else if (toState == LauncherState.BACKGROUND_APP) { + stateDepth.setValue(toState.getDepth(mLauncher)); + if (toState == LauncherState.BACKGROUND_APP) { mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); } } @Override public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, - PendingAnimation animation) { + PendingAnimation animation) { if (config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER) || mIgnoreStateChangesDuringMultiWindowAnimation) { return; } float toDepth = toState.getDepth(mLauncher); - if (Float.compare(mDepth, toDepth) != 0) { - animation.setFloat(this, DEPTH, toDepth, config.getInterpolator(ANIM_DEPTH, LINEAR)); - } + animation.setFloat(stateDepth, MULTI_PROPERTY_VALUE, toDepth, + config.getInterpolator(ANIM_DEPTH, LINEAR)); } - public void reapplyDepth() { - LauncherState toState = mLauncher.getStateManager().getState(); - float toDepth = toState.getDepth(mLauncher); - setDepth(toDepth, true); - } - - /** - * If we're launching an app from the home screen. - */ - public void setIsInLaunchTransition(boolean inLaunchTransition) { - boolean blurEnabled = SystemProperties.getBoolean("ro.launcher.blur.appLaunch", true); - mBlurDisabledForAppLaunch = inLaunchTransition && !blurEnabled; - if (!inLaunchTransition) { - // Reset depth at the end of the launch animation, so the wallpaper won't be - // zoomed out if an app crashes. - setDepth(0f); - } - } - - private void setDepth(float depth) { - setDepth(depth, false); - } - - private void setDepth(float depth, boolean force) { - if (!Utilities.ATLEAST_R) return; - depth = Utilities.boundToRange(depth, 0, 1); - // Round out the depth to dedupe frequent, non-perceptable updates - int depthI = (int) (depth * 256); - float depthF = depthI / 256f; - if (Float.compare(mDepth, depthF) == 0 && !force) { - return; - } - dispatchTransactionSurface(depthF); - mDepth = depthF; - } - - public void onOverlayScrollChanged(float progress) { - if (!Utilities.ATLEAST_R) return; - // Round out the progress to dedupe frequent, non-perceptable updates - int progressI = (int) (progress * 256); - float progressF = Utilities.boundToRange(progressI / 256f, 0f, 1f); - if (Float.compare(mOverlayScrollProgress, progressF) == 0) { - return; - } - mOverlayScrollProgress = progressF; - dispatchTransactionSurface(mDepth); - } - - private boolean dispatchTransactionSurface(float depth) { - boolean supportsBlur = BlurUtils.supportsBlursOnWindows(); - if (supportsBlur && (mSurface == null || !mSurface.isValid())) { - return false; - } + @Override + public void applyDepthAndBlur() { ensureDependencies(); - depth = Math.max(depth, mOverlayScrollProgress); - IBinder windowToken = mLauncher.getRootView().getWindowToken(); - if (windowToken != null) { - mWallpaperManager.setWallpaperZoomOut(windowToken, mEnableDepth ? depth : 1f); - } - - if (supportsBlur) { - boolean hasOpaqueBg = mLauncher.getScrimView().isFullyOpaque(); - boolean isSurfaceOpaque = !mHasContentBehindLauncher && hasOpaqueBg; - - mCurrentBlur = !mCrossWindowBlursEnabled || mBlurDisabledForAppLaunch || hasOpaqueBg - ? 0 : (int) (depth * mMaxBlurRadius); - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction() - .setBackgroundBlurRadius(mSurface, mCurrentBlur) - .setOpaque(mSurface, isSurfaceOpaque); - - // Set early wake-up flags when we know we're executing an expensive operation, this way - // SurfaceFlinger will adjust its internal offsets to avoid jank. - boolean wantsEarlyWakeUp = depth > 0 && depth < 1; - if (wantsEarlyWakeUp && !mInEarlyWakeUp) { - transaction.setEarlyWakeupStart(); - mInEarlyWakeUp = true; - } else if (!wantsEarlyWakeUp && mInEarlyWakeUp) { - transaction.setEarlyWakeupEnd(); - mInEarlyWakeUp = false; - } - - AttachedSurfaceControl rootSurfaceControl = - mLauncher.getRootView().getRootSurfaceControl(); - if (rootSurfaceControl != null) { - rootSurfaceControl.applyTransactionOnDraw(transaction); - } - } - return true; + super.applyDepthAndBlur(); } @Override public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { mIgnoreStateChangesDuringMultiWindowAnimation = true; - ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(this, DEPTH, - mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode)) + ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(stateDepth, MULTI_PROPERTY_VALUE, + mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode)) .setDuration(300); mwAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -404,12 +160,11 @@ public class DepthController implements StateHandler, writer.println(prefix + "\tmMaxBlurRadius=" + mMaxBlurRadius); writer.println(prefix + "\tmCrossWindowBlursEnabled=" + mCrossWindowBlursEnabled); writer.println(prefix + "\tmSurface=" + mSurface); - writer.println(prefix + "\tmOverlayScrollProgress=" + mOverlayScrollProgress); - writer.println(prefix + "\tmDepth=" + mDepth); + writer.println(prefix + "\tmStateDepth=" + stateDepth.getValue()); + writer.println(prefix + "\tmWidgetDepth=" + widgetDepth.getValue()); writer.println(prefix + "\tmCurrentBlur=" + mCurrentBlur); - writer.println(prefix + "\tmBlurDisabledForAppLaunch=" + mBlurDisabledForAppLaunch); writer.println(prefix + "\tmInEarlyWakeUp=" + mInEarlyWakeUp); writer.println(prefix + "\tmIgnoreStateChangesDuringMultiWindowAnimation=" + mIgnoreStateChangesDuringMultiWindowAnimation); } -} +} \ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java new file mode 100644 index 0000000000..d087d39955 --- /dev/null +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -0,0 +1,171 @@ +/* + * 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.statehandlers; + +import android.os.SystemProperties; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.uioverrides.QuickstepLauncher; + +/** + * Controls the visibility of the workspace and the resumed / paused state when desktop mode + * is enabled. + */ +public class DesktopVisibilityController { + + private static final String TAG = "DesktopVisController"; + private static final boolean DEBUG = false; + + private final Launcher mLauncher; + + private boolean mFreeformTasksVisible; + private boolean mInOverviewState; + private boolean mGestureInProgress; + + public DesktopVisibilityController(Launcher launcher) { + mLauncher = launcher; + } + + /** + * Whether desktop mode is supported. + */ + private boolean isDesktopModeSupported() { + return SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false) + || SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); + } + + /** + * Whether freeform windows are visible in desktop mode. + */ + public boolean areFreeformTasksVisible() { + return mFreeformTasksVisible; + } + + /** + * Sets whether freeform windows are visible and updates launcher visibility based on that. + */ + public void setFreeformTasksVisible(boolean freeformTasksVisible) { + if (DEBUG) { + Log.d(TAG, "setFreeformTasksVisible: visible=" + freeformTasksVisible); + } + if (!isDesktopModeSupported()) { + return; + } + if (freeformTasksVisible != mFreeformTasksVisible) { + mFreeformTasksVisible = freeformTasksVisible; + if (mFreeformTasksVisible) { + setLauncherViewsVisibility(View.INVISIBLE); + if (!mInOverviewState) { + // When freeform is visible & we're not in overview, we want launcher to appear + // paused, this ensures that taskbar displays. + markLauncherPaused(); + } + } else { + setLauncherViewsVisibility(View.VISIBLE); + // If freeform isn't visible ensure that launcher appears resumed to behave + // normally. + markLauncherResumed(); + } + } + } + + /** + * Sets whether the overview is visible and updates launcher visibility based on that. + */ + public void setOverviewStateEnabled(boolean overviewStateEnabled) { + if (DEBUG) { + Log.d(TAG, "setOverviewStateEnabled: enabled=" + overviewStateEnabled); + } + if (!isDesktopModeSupported()) { + return; + } + if (overviewStateEnabled != mInOverviewState) { + mInOverviewState = overviewStateEnabled; + if (mInOverviewState) { + setLauncherViewsVisibility(View.VISIBLE); + markLauncherResumed(); + } else if (mFreeformTasksVisible) { + setLauncherViewsVisibility(View.INVISIBLE); + markLauncherPaused(); + } + } + } + + /** + * Whether recents gesture is currently in progress. + */ + public boolean isGestureInProgress() { + return mGestureInProgress; + } + + /** + * Sets whether recents gesture is in progress. + */ + public void setGestureInProgress(boolean gestureInProgress) { + if (DEBUG) { + Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress); + } + if (!isDesktopModeSupported()) { + return; + } + if (gestureInProgress != mGestureInProgress) { + mGestureInProgress = gestureInProgress; + } + } + + private void setLauncherViewsVisibility(int visibility) { + if (DEBUG) { + Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility); + } + View workspaceView = mLauncher.getWorkspace(); + if (workspaceView != null) { + workspaceView.setVisibility(visibility); + } + View dragLayer = mLauncher.getDragLayer(); + if (dragLayer != null) { + dragLayer.setVisibility(visibility); + } + } + + private void markLauncherPaused() { + if (DEBUG) { + Log.d(TAG, "markLauncherPaused"); + } + StatefulActivity activity = + QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity(); + if (activity != null) { + activity.setPaused(); + } + } + + private void markLauncherResumed() { + if (DEBUG) { + Log.d(TAG, "markLauncherResumed"); + } + StatefulActivity activity = + QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity(); + // Check activity state before calling setResumed(). Launcher may have been actually + // paused (eg fullscreen task moved to front). + // In this case we should not mark the activity as resumed. + if (activity != null && activity.isResumed()) { + activity.setResumed(); + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java new file mode 100644 index 0000000000..331184a081 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java @@ -0,0 +1,74 @@ +/* + * 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; + +import android.content.Context; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; + +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.LauncherPrefs; +import com.android.launcher3.util.OnboardingPrefs; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; + +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 { + + protected final LayoutInflater mLayoutInflater; + private final List mDPChangeListeners = new ArrayList<>(); + private final OnboardingPrefs mOnboardingPrefs; + + public BaseTaskbarContext(Context windowContext) { + super(windowContext, Themes.getActivityThemeRes(windowContext)); + mLayoutInflater = LayoutInflater.from(this).cloneInContext(this); + mOnboardingPrefs = new OnboardingPrefs<>(this, LauncherPrefs.getPrefs(this)); + } + + @Override + public final LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + @Override + public final List getOnDeviceProfileChangeListeners() { + return mDPChangeListeners; + } + + @Override + public OnboardingPrefs getOnboardingPrefs() { + return mOnboardingPrefs; + } + + /** Callback invoked when a drag is initiated within this context. */ + public abstract void onDragStart(); + + /** Callback invoked when a drag is finished within this context. */ + public abstract void onDragEnd(); + + /** Callback invoked when a popup is shown or closed within this context. */ + public abstract void onPopupVisibilityChanged(boolean isVisible); + + /** + * Callback invoked when user attempts to split the screen through a long-press menu in Taskbar + * or AllApps. + */ + public abstract void onSplitScreenMenuButtonClicked(); +} diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java new file mode 100644 index 0000000000..268024fd3d --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java @@ -0,0 +1,77 @@ +/* + * 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; + +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_NOTIFICATIONS; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_QUICK_SETTINGS; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.launcher3.R; + +/** + * Controller for managing buttons and status icons in taskbar in a desktop environment. + */ +public class DesktopNavbarButtonsViewController extends NavbarButtonsViewController { + + private final TaskbarActivityContext mContext; + private final FrameLayout mNavButtonsView; + private final ViewGroup mNavButtonContainer; + private final ViewGroup mStartContextualContainer; + private final View mAllAppsButton; + + private TaskbarControllers mControllers; + + public DesktopNavbarButtonsViewController(TaskbarActivityContext context, + FrameLayout navButtonsView) { + super(context, navButtonsView); + mContext = context; + mNavButtonsView = navButtonsView; + mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons); + mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons); + mAllAppsButton = LayoutInflater.from(context) + .inflate(R.layout.taskbar_all_apps_button, mStartContextualContainer, false); + mAllAppsButton.setOnClickListener((View v) -> { + mControllers.taskbarAllAppsController.show(); + }); + } + + /** + * Initializes the controller + */ + @Override + public void init(TaskbarControllers controllers) { + mControllers = controllers; + mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarHeight; + + // Quick settings and notifications buttons + addButton(R.drawable.ic_sysbar_quick_settings, BUTTON_QUICK_SETTINGS, + mNavButtonContainer, mControllers.navButtonController, + R.id.quick_settings_button); + addButton(R.drawable.ic_sysbar_notifications, BUTTON_NOTIFICATIONS, + mNavButtonContainer, mControllers.navButtonController, + R.id.notifications_button); + // All apps button + mStartContextualContainer.addView(mAllAppsButton); + } + + /** Cleans up on destroy */ + @Override + public void onDestroy() { } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java new file mode 100644 index 0000000000..acfbea38b7 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java @@ -0,0 +1,153 @@ +/* + * 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; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.util.SparseArray; + +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.quickstep.RecentsModel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Provides recent apps functionality specifically in a desktop environment. + */ +public class DesktopTaskbarRecentAppsController extends TaskbarRecentAppsController { + + private final TaskbarActivityContext mContext; + private ArrayList mRunningApps = new ArrayList<>(); + private AppInfo[] mApps; + + public DesktopTaskbarRecentAppsController(TaskbarActivityContext context) { + mContext = context; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mApps = null; + } + + @Override + protected void setApps(AppInfo[] apps) { + mApps = apps; + } + + @Override + protected boolean isEnabled() { + return true; + } + + /** + * Set mRunningApps to hold currently running applications using the list of currently running + * tasks. Filtering is also done to ignore applications that are already on the taskbar in the + * original hotseat. + */ + @Override + protected void updateRunningApps(SparseArray hotseatItems) { + ArrayList runningApps = getRunningAppsFromTasks(); + ArrayList filteredRunningApps = new ArrayList<>(); + for (AppInfo runningApp : runningApps) { + boolean shouldAddOnTaskbar = true; + for (int i = 0; i < hotseatItems.size(); i++) { + if (hotseatItems.keyAt(i) >= mControllers.taskbarActivityContext.getDeviceProfile() + .numShownHotseatIcons) { + break; + } + if (hotseatItems.valueAt(i).getTargetPackage() + .equals(runningApp.getTargetPackage())) { + shouldAddOnTaskbar = false; + break; + } + } + if (shouldAddOnTaskbar) { + filteredRunningApps.add(new WorkspaceItemInfo(runningApp)); + } + } + mRunningApps = filteredRunningApps; + mControllers.taskbarViewController.commitRunningAppsToUI(); + } + + /** + * Returns a copy of hotseatItems with the addition of currently running applications. + */ + @Override + protected ItemInfo[] updateHotseatItemInfos(ItemInfo[] hotseatItemInfos) { + // hotseatItemInfos.length would be 0 if deviceProfile.numShownHotseatIcons is 0, so we + // don't want to show anything in the hotseat + if (hotseatItemInfos.length == 0) return hotseatItemInfos; + + int runningAppsIndex = 0; + ItemInfo[] newHotseatItemsInfo = Arrays.copyOf( + hotseatItemInfos, hotseatItemInfos.length + mRunningApps.size()); + for (int i = hotseatItemInfos.length; i < newHotseatItemsInfo.length; i++) { + newHotseatItemsInfo[i] = mRunningApps.get(runningAppsIndex); + runningAppsIndex++; + } + return newHotseatItemsInfo; + } + + + /** + * Returns a list of running applications from the list of currently running tasks. + */ + private ArrayList getRunningAppsFromTasks() { + ArrayList tasks = + RecentsModel.INSTANCE.get(mContext).getRunningTasks(); + ArrayList runningApps = new ArrayList<>(); + // early return if apps is empty, since we would have no AppInfo to compare + if (mApps == null) { + return runningApps; + } + + Set seenPackages = new HashSet<>(); + for (ActivityManager.RunningTaskInfo taskInfo : tasks) { + if (taskInfo.realActivity == null) continue; + + // If a different task for the same package has already been handled, skip this one + String taskPackage = taskInfo.realActivity.getPackageName(); + if (seenPackages.contains(taskPackage)) continue; + + // Otherwise, get the corresponding AppInfo and add it to the list + seenPackages.add(taskPackage); + AppInfo app = getAppInfo(taskInfo.realActivity); + if (app == null) continue; + runningApps.add(app); + } + return runningApps; + } + + /** + * Retrieves the corresponding AppInfo for the activity. + */ + private AppInfo getAppInfo(ComponentName activity) { + String packageName = activity.getPackageName(); + for (AppInfo app : mApps) { + if (!packageName.equals(app.getTargetPackage())) { + continue; + } + return app; + } + return null; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java new file mode 100644 index 0000000000..633325bf4b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.uioverrides.QuickstepLauncher; + +/** + * A data source which integrates with a Launcher instance, used specifically for a + * desktop environment. + */ +public class DesktopTaskbarUIController extends TaskbarUIController { + + private final QuickstepLauncher mLauncher; + + public DesktopTaskbarUIController(QuickstepLauncher launcher) { + mLauncher = launcher; + } + + @SuppressWarnings("MissingSuperCall") // TODO: Fix me + @Override + protected void init(TaskbarControllers taskbarControllers) { + super.init(taskbarControllers); + mLauncher.getHotseat().setIconsAlpha(0f); + mControllers.taskbarViewController.updateRunningApps(); + } + + @SuppressWarnings("MissingSuperCall") // TODO: Fix me + @Override + protected void onDestroy() { + super.onDestroy(); + mLauncher.getHotseat().setIconsAlpha(1f); + } + + /** Disable taskbar stashing in desktop environment. */ + @Override + public boolean supportsVisualStashing() { + return false; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java index f1e67479f5..ed4a212590 100644 --- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java @@ -15,12 +15,13 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.Utilities.isRunningInTestHarness; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE; -import static com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION; import android.animation.Animator; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager; import com.android.quickstep.RecentsActivity; import com.android.quickstep.fallback.RecentsState; @@ -40,10 +41,20 @@ public class FallbackTaskbarUIController extends TaskbarUIController { animateToRecentsState(toState); // Handle tapping on live tile. - RecentsView recentsView = mRecentsActivity.getOverviewPanel(); - recentsView.setTaskLaunchListener(toState == RecentsState.DEFAULT + getRecentsView().setTaskLaunchListener(toState == RecentsState.DEFAULT ? (() -> animateToRecentsState(RecentsState.BACKGROUND_APP)) : null); } + + @Override + public void onStateTransitionComplete(RecentsState finalState) { + boolean finalStateDefault = finalState == RecentsState.DEFAULT; + // TODO(b/268120202) Taskbar shows up on 3P home, currently we don't go to + // overview from 3P home. Either implement that or it'll change w/ contextual? + boolean disallowLongClick = finalState == RecentsState.OVERVIEW_SPLIT_SELECT; + Utilities.setOverviewDragState(mControllers, + finalStateDefault /*disallowGlobalDrag*/, disallowLongClick, + finalStateDefault /*allowInitialSplitSelection*/); + } }; public FallbackTaskbarUIController(RecentsActivity recentsActivity) { @@ -70,19 +81,31 @@ public class FallbackTaskbarUIController extends TaskbarUIController { * Currently this animation just force stashes the taskbar in Overview. */ public Animator createAnimToRecentsState(RecentsState toState, long duration) { - boolean forceStashed = toState.hasOverviewActions(); - TaskbarStashController controller = mControllers.taskbarStashController; + // Force stash the taskbar in overview modal state or when going home. We do not force + // stash on home when running in a test as 3p launchers rely on taskbar instead of hotseat. + boolean isGoingHome = toState == RecentsState.HOME && !isRunningInTestHarness(); + boolean useStashedLauncherState = toState.hasOverviewActions() || isGoingHome; + boolean stashedLauncherState = useStashedLauncherState && ( + (FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW.get() && toState == RecentsState.MODAL_TASK) + || isGoingHome); + TaskbarStashController stashController = mControllers.taskbarStashController; // Set both FLAG_IN_STASHED_LAUNCHER_STATE and FLAG_IN_APP to ensure the state is respected. // For all other states, just use the current stashed-in-app setting (e.g. if long clicked). - controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, forceStashed); - controller.updateStateForFlag(FLAG_IN_APP, !forceStashed); - return controller.applyStateWithoutStart(duration); + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, stashedLauncherState); + stashController.updateStateForFlag(FLAG_IN_APP, !useStashedLauncherState); + return stashController.createApplyStateAnimator(duration); } private void animateToRecentsState(RecentsState toState) { - Animator anim = createAnimToRecentsState(toState, TASKBAR_STASH_DURATION); + Animator anim = createAnimToRecentsState(toState, + mControllers.taskbarStashController.getStashDuration()); if (anim != null) { anim.start(); } } + + @Override + public RecentsView getRecentsView() { + return mRecentsActivity.getOverviewPanel(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java new file mode 100644 index 0000000000..c4962cd016 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.R; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Handles initialization of the {@link KeyboardQuickSwitchViewController}. + */ +public final class KeyboardQuickSwitchController implements + TaskbarControllers.LoggableTaskbarController { + + static final int MAX_TASKS = 6; + + @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks(); + + // Initialized on init + @Nullable private RecentsModel mModel; + + // 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 int mTaskListChangeId = -1; + // Only empty before the recent tasks list has been loaded the first time + @NonNull private List mTasks = new ArrayList<>(); + private int mNumHiddenTasks = 0; + + // Initialized in init + private TaskbarControllers mControllers; + + @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; + + /** Initialize the controller. */ + public void init(@NonNull TaskbarControllers controllers) { + mControllers = controllers; + mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext); + } + + void onConfigurationChanged(@ActivityInfo.Config int configChanges) { + if (mQuickSwitchViewController == null) { + return; + } + if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD + | ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) { + mQuickSwitchViewController.closeQuickSwitchView(true); + return; + } + int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex(); + onDestroy(); + if (currentFocusedIndex != -1) { + mControllers.taskbarActivityContext.getMainThreadHandler().post( + () -> openQuickSwitchView(currentFocusedIndex)); + } + } + + void openQuickSwitchView() { + openQuickSwitchView(-1); + } + + private void openQuickSwitchView(int currentFocusedIndex) { + if (mQuickSwitchViewController != null) { + return; + } + TaskbarOverlayContext overlayContext = + mControllers.taskbarOverlayController.requestWindow(); + KeyboardQuickSwitchView keyboardQuickSwitchView = + (KeyboardQuickSwitchView) overlayContext.getLayoutInflater() + .inflate( + R.layout.keyboard_quick_switch_view, + overlayContext.getDragLayer(), + /* attachToRoot= */ false); + mQuickSwitchViewController = new KeyboardQuickSwitchViewController( + mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); + + if (mModel.isTaskListValid(mTaskListChangeId)) { + mQuickSwitchViewController.openQuickSwitchView( + mTasks, mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex); + return; + } + mTaskListChangeId = mModel.getTasks((tasks) -> { + // Only store MAX_TASK tasks, from most to least recent + Collections.reverse(tasks); + mTasks = tasks.stream().limit(MAX_TASKS).collect(Collectors.toList()); + mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); + mQuickSwitchViewController.openQuickSwitchView( + mTasks, mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex); + }); + } + + void closeQuickSwitchView() { + if (mQuickSwitchViewController == null) { + return; + } + mQuickSwitchViewController.closeQuickSwitchView(true); + } + + /** + * See {@link TaskbarUIController#launchFocusedTask()} + */ + int launchFocusedTask() { + // Return -1 so that the RecentsView is not incorrectly opened when the user closes the + // quick switch view by tapping the screen. + return mQuickSwitchViewController == null + ? -1 : mQuickSwitchViewController.launchFocusedTask(); + } + + void onDestroy() { + if (mQuickSwitchViewController != null) { + mQuickSwitchViewController.onDestroy(); + } + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "KeyboardQuickSwitchController:"); + + pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); + pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); + pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); + pw.println(prefix + "\tmTasks=["); + for (GroupTask task : mTasks) { + Task task1 = task.task1; + Task task2 = task.task2; + ComponentName cn1 = task1.getTopComponent(); + ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; + pw.println(prefix + "\t\tt1: (id=" + task1.key.id + + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") + + " t2: (id=" + (task2 != null ? task2.key.id : "-1") + + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" + : "no package)")); + } + pw.println(prefix + "\t]"); + + if (mQuickSwitchViewController != null) { + mQuickSwitchViewController.dumpLogs(prefix + '\t', pw); + } + } + + class ControllerCallbacks { + + int getTaskCount() { + return mNumHiddenTasks == 0 ? mTasks.size() : MAX_TASKS + 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); + } + + void updateTitleInBackground(Task task, Consumer callback) { + mModel.getIconCache().updateIconInBackground(task, callback); + } + + void onCloseComplete() { + mQuickSwitchViewController = null; + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java new file mode 100644 index 0000000000..84129fdde4 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; + +import android.animation.Animator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.launcher3.R; +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; + +/** + * A view that displays a recent task during a keyboard quick switch. + */ +public class KeyboardQuickSwitchTaskView extends ConstraintLayout { + + @NonNull private final BorderAnimator mBorderAnimator; + + @Nullable private ImageView mThumbnailView1; + @Nullable private ImageView mThumbnailView2; + + public KeyboardQuickSwitchTaskView(@NonNull Context context) { + this(context, null); + } + + public KeyboardQuickSwitchTaskView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardQuickSwitchTaskView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardQuickSwitchTaskView( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setWillNotDraw(false); + Resources resources = context.getResources(); + mBorderAnimator = new BorderAnimator( + /* borderBoundsBuilder= */ bounds -> bounds.set(0, 0, getWidth(), getHeight()), + /* borderWidthPx= */ resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width), + /* borderRadiusPx= */ resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius), + /* borderColor= */ attrs == null + ? DEFAULT_BORDER_COLOR + : context.getTheme() + .obtainStyledAttributes( + attrs, + R.styleable.TaskView, + defStyleAttr, + defStyleRes) + .getColor( + R.styleable.TaskView_borderColor, + DEFAULT_BORDER_COLOR), + /* invalidateViewCallback= */ KeyboardQuickSwitchTaskView.this::invalidate); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mThumbnailView1 = findViewById(R.id.thumbnail1); + mThumbnailView2 = findViewById(R.id.thumbnail2); + } + + @NonNull + protected Animator getFocusAnimator(boolean focused) { + return mBorderAnimator.buildAnimator(focused); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + mBorderAnimator.drawBorder(canvas); + } + + protected void setThumbnails( + @NonNull Task task1, + @Nullable Task task2, + @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction, + @Nullable TitleUpdateFunction titleUpdateFunction) { + applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction); + applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction); + + if (titleUpdateFunction == null) { + setContentDescription(task2 == null + ? task1.titleDescription + : getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription)); + return; + } + titleUpdateFunction.updateTitleInBackground(task1, t -> + setContentDescription(task1.titleDescription)); + if (task2 == null) { + return; + } + titleUpdateFunction.updateTitleInBackground(task2, t -> + setContentDescription(getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription))); + } + + private void applyThumbnail( + @Nullable ImageView thumbnailView, + @Nullable Task task, + @Nullable ThumbnailUpdateFunction updateFunction) { + if (thumbnailView == null) { + return; + } + if (task == null) { + return; + } + if (updateFunction == null) { + applyThumbnail(thumbnailView, task.thumbnail); + return; + } + updateFunction.updateThumbnailInBackground( + task, thumbnailData -> applyThumbnail(thumbnailView, thumbnailData)); + } + + private void applyThumbnail( + @NonNull ImageView thumbnailView, ThumbnailData thumbnailData) { + Bitmap bm = thumbnailData == null ? null : thumbnailData.thumbnail; + + thumbnailView.setVisibility(VISIBLE); + thumbnailView.setImageBitmap(bm); + } + + protected interface ThumbnailUpdateFunction { + + void updateThumbnailInBackground(Task task, Consumer callback); + } + + protected interface TitleUpdateFunction { + + void updateTitleInBackground(Task task, Consumer callback); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java new file mode 100644 index 0000000000..745defc18b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; + +import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Rect; +import android.icu.text.MessageFormat; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.animation.Interpolator; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.Interpolators; +import com.android.quickstep.util.GroupTask; + +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. + */ +public class KeyboardQuickSwitchView extends ConstraintLayout { + + private static final long OUTLINE_ANIMATION_DURATION_MS = 333; + private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f; + private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f; + private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; + + private static final long ALPHA_ANIMATION_DURATION_MS = 83; + private static final long ALPHA_ANIMATION_START_DELAY_MS = 67; + + private static final long CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS = 500; + private static final long CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS = 333; + private static final float CONTENT_START_TRANSLATION_X_DP = 32; + private static final float CONTENT_START_TRANSLATION_Y_DP = 40; + private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED; + private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; + + private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83; + private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83; + + private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat( + this::invalidateOutline); + + private HorizontalScrollView mScrollView; + private ConstraintLayout mContent; + + private int mTaskViewHeight; + private int mSpacing; + private int mOutlineRadius; + private boolean mIsRtl; + + @Nullable private AnimatorSet mOpenAnimation; + + @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; + + public KeyboardQuickSwitchView(@NonNull Context context) { + this(context, null); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mScrollView = findViewById(R.id.scroll_view); + mContent = findViewById(R.id.content); + + Resources resources = getResources(); + mTaskViewHeight = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_taskview_height); + mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); + mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); + mIsRtl = Utilities.isRtl(resources); + } + + @NonNull + private KeyboardQuickSwitchTaskView createAndAddTaskView( + int index, + int width, + boolean isFinalView, + boolean updateTasks, + @NonNull LayoutInflater layoutInflater, + @Nullable View previousView, + @NonNull List groupTasks) { + KeyboardQuickSwitchTaskView taskView = (KeyboardQuickSwitchTaskView) layoutInflater.inflate( + R.layout.keyboard_quick_switch_taskview, mContent, false); + taskView.setId(View.generateViewId()); + taskView.setOnClickListener(v -> mViewCallbacks.launchTappedTask(index)); + + LayoutParams lp = new LayoutParams(width, mTaskViewHeight); + // Create a right-to-left ordering of views (or left-to-right in RTL locales) + if (previousView != null) { + lp.endToStart = previousView.getId(); + } else { + lp.endToEnd = PARENT_ID; + } + lp.topToTop = PARENT_ID; + lp.bottomToBottom = PARENT_ID; + // Add spacing between views + lp.setMarginEnd(mSpacing); + if (isFinalView) { + // Add spacing to the start of the final view so that scrolling ends with some padding. + lp.startToStart = PARENT_ID; + lp.setMarginStart(mSpacing); + lp.horizontalBias = 1f; + } + + GroupTask groupTask = groupTasks.get(index); + taskView.setThumbnails( + groupTask.task1, + groupTask.task2, + updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, + updateTasks ? mViewCallbacks::updateTitleInBackground : null); + + mContent.addView(taskView, lp); + return taskView; + } + + private void createAndAddOverviewButton( + int width, + @NonNull LayoutInflater layoutInflater, + @Nullable View previousView, + @NonNull String overflowString) { + KeyboardQuickSwitchTaskView overviewButton = + (KeyboardQuickSwitchTaskView) layoutInflater.inflate( + R.layout.keyboard_quick_switch_overview, this, false); + overviewButton.setOnClickListener(v -> mViewCallbacks.launchTappedTask(MAX_TASKS)); + + overviewButton.findViewById(R.id.text).setText(overflowString); + + ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( + width, mTaskViewHeight); + lp.startToStart = PARENT_ID; + lp.endToStart = previousView.getId(); + lp.topToTop = PARENT_ID; + lp.bottomToBottom = PARENT_ID; + lp.setMarginEnd(mSpacing); + lp.setMarginStart(mSpacing); + + mContent.addView(overviewButton, lp); + } + + protected void applyLoadPlan( + @NonNull Context context, + @NonNull List groupTasks, + int numHiddenTasks, + boolean updateTasks, + int currentFocusIndexOverride, + @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) { + if (groupTasks.isEmpty()) { + // Do not show the quick switch view. + return; + } + mViewCallbacks = viewCallbacks; + Resources resources = context.getResources(); + int width = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_taskview_width); + View previousView = null; + + LayoutInflater layoutInflater = LayoutInflater.from(context); + int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size()); + for (int i = 0; i < tasksToDisplay; i++) { + previousView = createAndAddTaskView( + i, + width, + /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0, + updateTasks, + layoutInflater, + previousView, + groupTasks); + } + + if (numHiddenTasks > 0) { + HashMap args = new HashMap<>(); + args.put("count", numHiddenTasks); + createAndAddOverviewButton( + width, + layoutInflater, + previousView, + new MessageFormat( + resources.getString(R.string.quick_switch_overflow), + Locale.getDefault()).format(args)); + } + + getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + animateOpen(currentFocusIndexOverride); + + getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + + protected Animator getCloseAnimation() { + AnimatorSet closeAnimation = new AnimatorSet(); + + Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(0f); + outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); + outlineAnimation.setInterpolator(CLOSE_OUTLINE_INTERPOLATOR); + closeAnimation.play(outlineAnimation); + + Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0f); + alphaAnimation.setStartDelay(ALPHA_ANIMATION_START_DELAY_MS); + alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); + closeAnimation.play(alphaAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_Y, 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); + closeAnimation.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 1f, 0f); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + closeAnimation.play(contentAlphaAnimation); + + closeAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mOpenAnimation != null) { + mOpenAnimation.cancel(); + } + } + }); + + return closeAnimation; + } + + private void animateOpen(int currentFocusIndexOverride) { + if (mOpenAnimation != null) { + // Restart animation since currentFocusIndexOverride can change the initial scroll. + mOpenAnimation.cancel(); + } + mOpenAnimation = new AnimatorSet(); + + Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f); + outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); + mOpenAnimation.play(outlineAnimation); + + Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0f, 1f); + alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); + mOpenAnimation.play(alphaAnimation); + + Animator translationXAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_X, -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); + translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); + translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); + mOpenAnimation.play(translationXAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_Y, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); + mOpenAnimation.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 0f, 1f); + contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + mOpenAnimation.play(contentAlphaAnimation); + + ViewOutlineProvider outlineProvider = getOutlineProvider(); + mOpenAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + setClipToPadding(false); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect( + /* rect= */ new Rect( + /* left= */ 0, + /* top= */ 0, + /* right= */ getWidth(), + /* bottom= */ + (int) (getHeight() * Utilities.mapBoundToRange( + mOutlineAnimationProgress.value, + /* lowerBound= */ 0f, + /* upperBound= */ 1f, + /* toMin= */ OUTLINE_START_HEIGHT_FACTOR, + /* toMax= */ 1f, + OPEN_OUTLINE_INTERPOLATOR))), + /* radius= */ mOutlineRadius * Utilities.mapBoundToRange( + mOutlineAnimationProgress.value, + /* lowerBound= */ 0f, + /* upperBound= */ 1f, + /* toMin= */ OUTLINE_START_RADIUS_FACTOR, + /* toMax= */ 1f, + OPEN_OUTLINE_INTERPOLATOR)); + } + }); + if (currentFocusIndexOverride == -1) { + initializeScroll(/* index= */ 0, /* shouldTruncateTarget= */ false); + } else { + animateFocusMove(-1, currentFocusIndexOverride); + } + mScrollView.setVisibility(VISIBLE); + setVisibility(VISIBLE); + requestFocus(); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + setClipToPadding(true); + setOutlineProvider(outlineProvider); + invalidateOutline(); + mOpenAnimation = null; + } + }); + + mOpenAnimation.start(); + } + + protected void animateFocusMove(int fromIndex, int toIndex) { + KeyboardQuickSwitchTaskView focusedTask = getTaskAt(toIndex); + if (focusedTask == null) { + return; + } + AnimatorSet focusAnimation = new AnimatorSet(); + focusAnimation.play(focusedTask.getFocusAnimator(true)); + + KeyboardQuickSwitchTaskView previouslyFocusedTask = getTaskAt(fromIndex); + if (previouslyFocusedTask != null) { + focusAnimation.play(previouslyFocusedTask.getFocusAnimator(false)); + } + + focusAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + focusedTask.requestAccessibilityFocus(); + if (fromIndex == -1) { + int firstVisibleTaskIndex = toIndex == 0 + ? toIndex + : getTaskAt(toIndex - 1) == null + ? toIndex : toIndex - 1; + // Scroll so that the previous task view is truncated as a visual hint that + // there are more tasks + initializeScroll( + firstVisibleTaskIndex, + /* shouldTruncateTarget= */ firstVisibleTaskIndex != toIndex); + } else if (toIndex > fromIndex || toIndex == 0) { + // Scrolling to next task view + if (mIsRtl) { + scrollRightTo(focusedTask); + } else { + scrollLeftTo(focusedTask); + } + } else { + // Scrolling to previous task view + if (mIsRtl) { + scrollLeftTo(focusedTask); + } else { + scrollRightTo(focusedTask); + } + } + if (mViewCallbacks != null) { + mViewCallbacks.updateCurrentFocusIndex(toIndex); + } + } + }); + + focusAnimation.start(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return (mViewCallbacks != null && mViewCallbacks.onKeyUp(keyCode, event, mIsRtl)) + || super.onKeyUp(keyCode, event); + } + + private void initializeScroll(int index, boolean shouldTruncateTarget) { + View task = getTaskAt(index); + if (task == null) { + return; + } + if (mIsRtl) { + scrollRightTo( + task, shouldTruncateTarget, /* smoothScroll= */ false); + } else { + scrollLeftTo( + task, shouldTruncateTarget, /* smoothScroll= */ false); + } + } + + private void scrollRightTo(@NonNull View targetTask) { + scrollRightTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true); + } + + private void scrollRightTo( + @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) { + if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { + return; + } + int scrollTo = targetTask.getLeft() - mSpacing + + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); + // Scroll so that the focused task is to the left of the list + if (smoothScroll) { + mScrollView.smoothScrollTo(scrollTo, 0); + } else { + mScrollView.scrollTo(scrollTo, 0); + } + } + + private void scrollLeftTo(@NonNull View targetTask) { + scrollLeftTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true); + } + + private void scrollLeftTo( + @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) { + if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { + return; + } + int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth() + - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); + // Scroll so that the focused task is to the right of the list + if (smoothScroll) { + mScrollView.smoothScrollTo(scrollTo, 0); + } else { + mScrollView.scrollTo(scrollTo, 0); + } + } + + private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) { + boolean isTargetTruncated = + targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth() + || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX(); + + return isTargetTruncated && !shouldTruncateTarget; + } + + @Nullable + protected KeyboardQuickSwitchTaskView getTaskAt(int index) { + return index < 0 || index >= mContent.getChildCount() + ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java new file mode 100644 index 0000000000..c1f764f148 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.animation.Animator; +import android.view.KeyEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +import java.io.PrintWriter; +import java.util.List; +import java.util.function.Consumer; + +/** + * Handles initialization of the {@link KeyboardQuickSwitchView} and supplies it with the list of + * tasks. + */ +public class KeyboardQuickSwitchViewController { + + @NonNull private final ViewCallbacks mViewCallbacks = new ViewCallbacks(); + @NonNull private final TaskbarControllers mControllers; + @NonNull private final TaskbarOverlayContext mOverlayContext; + @NonNull private final KeyboardQuickSwitchView mKeyboardQuickSwitchView; + @NonNull private final KeyboardQuickSwitchController.ControllerCallbacks mControllerCallbacks; + + @Nullable private Animator mCloseAnimation; + + private int mCurrentFocusIndex = -1; + + protected KeyboardQuickSwitchViewController( + @NonNull TaskbarControllers controllers, + @NonNull TaskbarOverlayContext overlayContext, + @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView, + @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks) { + mControllers = controllers; + mOverlayContext = overlayContext; + mKeyboardQuickSwitchView = keyboardQuickSwitchView; + mControllerCallbacks = controllerCallbacks; + } + + protected int getCurrentFocusedIndex() { + return mCurrentFocusIndex; + } + + protected void openQuickSwitchView( + @NonNull List tasks, + int numHiddenTasks, + boolean updateTasks, + int currentFocusIndexOverride) { + TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer(); + dragLayer.addView(mKeyboardQuickSwitchView); + dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true)); + + mKeyboardQuickSwitchView.applyLoadPlan( + mOverlayContext, + tasks, + numHiddenTasks, + updateTasks, + currentFocusIndexOverride, + mViewCallbacks); + } + + protected void closeQuickSwitchView(boolean animate) { + if (mCloseAnimation != null) { + if (animate) { + // Let currently-running animation finish. + return; + } else { + mCloseAnimation.cancel(); + } + } + if (!animate) { + mCloseAnimation = null; + onCloseComplete(); + return; + } + mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation(); + + mCloseAnimation.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + mCloseAnimation = null; + onCloseComplete(); + } + }); + mCloseAnimation.start(); + } + + /** + * Launched the currently-focused task. + * + * Returns index -1 iff the RecentsView shouldn't be opened. + * + * If the index is not -1, then the {@link com.android.quickstep.views.TaskView} at the returned + * index will be focused. + */ + protected int launchFocusedTask() { + // Launch the second-most recent task if the user quick switches too quickly, if possible. + return launchTaskAt(mCurrentFocusIndex == -1 + ? (mControllerCallbacks.getTaskCount() > 1 ? 1 : 0) : mCurrentFocusIndex); + } + + private int launchTaskAt(int index) { + if (mCloseAnimation != null) { + // Ignore taps on task views and alt key unpresses while the close animation is running. + 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. + View taskView = mKeyboardQuickSwitchView.getTaskAt(index); + GroupTask task = mControllerCallbacks.getTaskAt(index); + if (task == null) { + return Math.max(0, index); + } else if (task.task2 == null) { + UI_HELPER_EXECUTOR.execute(() -> + ActivityManagerWrapper.getInstance().startActivityFromRecents( + task.task1.key, + mControllers.taskbarActivityContext.getActivityLaunchOptions( + taskView == null ? mKeyboardQuickSwitchView : taskView, null) + .options)); + } else { + mControllers.uiController.launchSplitTasks( + taskView == null ? mKeyboardQuickSwitchView : taskView, task); + } + return -1; + } + + private void onCloseComplete() { + mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); + mControllerCallbacks.onCloseComplete(); + } + + protected void onDestroy() { + closeQuickSwitchView(false); + } + + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "KeyboardQuickSwitchViewController:"); + + pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus()); + pw.println(prefix + "\tcloseAnimationRunning=" + (mCloseAnimation != null)); + pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex); + } + + class ViewCallbacks { + + boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL) { + if (keyCode != KeyEvent.KEYCODE_TAB + && keyCode != KeyEvent.KEYCODE_DPAD_RIGHT + && keyCode != KeyEvent.KEYCODE_DPAD_LEFT + && keyCode != KeyEvent.KEYCODE_GRAVE + && keyCode != KeyEvent.KEYCODE_ESCAPE) { + return false; + } + if (keyCode == KeyEvent.KEYCODE_GRAVE || keyCode == KeyEvent.KEYCODE_ESCAPE) { + closeQuickSwitchView(true); + return true; + } + 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 toIndex = mCurrentFocusIndex == -1 + // Focus the second-most recent app if possible + ? (taskCount > 1 ? 1 : 0) + : (traverseBackwards + // focus a more recent task or loop back to the opposite end + ? Math.max(0, mCurrentFocusIndex == 0 + ? taskCount - 1 : mCurrentFocusIndex - 1) + // focus a less recent app or loop back to the opposite end + : ((mCurrentFocusIndex + 1) % taskCount)); + + mKeyboardQuickSwitchView.animateFocusMove(mCurrentFocusIndex, toIndex); + + return true; + } + + void updateCurrentFocusIndex(int index) { + mCurrentFocusIndex = index; + } + + void launchTappedTask(int index) { + KeyboardQuickSwitchViewController.this.launchTaskAt(index); + closeQuickSwitchView(true); + } + + void updateThumbnailInBackground(Task task, Consumer callback) { + mControllerCallbacks.updateThumbnailInBackground(task, callback); + } + + void updateTitleInBackground(Task task, Consumer callback) { + mControllerCallbacks.updateTitleInBackground(task, callback); + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 5e8db69f2b..fbf78ae1ee 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -15,37 +15,44 @@ */ package com.android.launcher3.taskbar; +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; + +import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION; +import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_EDU_TOOLTIP; +import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES; import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_RESUMED; -import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR; +import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import android.animation.Animator; -import android.annotation.ColorInt; +import android.animation.AnimatorSet; import android.os.RemoteException; import android.util.Log; -import android.view.MotionEvent; import android.view.TaskTransitionSpec; +import android.view.View; import android.view.WindowManagerGlobal; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherState; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; -import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.ItemInfo; +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.AnimatedFloat; import com.android.quickstep.RecentsAnimationCallbacks; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.RecentsView; -import java.util.Arrays; +import java.io.PrintWriter; import java.util.Set; -import java.util.stream.Stream; /** * A data source which integrates with a Launcher instance @@ -54,27 +61,34 @@ public class LauncherTaskbarUIController extends TaskbarUIController { private static final String TAG = "TaskbarUIController"; - private final BaseQuickstepLauncher mLauncher; + public static final int MINUS_ONE_PAGE_PROGRESS_INDEX = 0; + public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1; + public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2; + public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3; + + public static final int DISPLAY_PROGRESS_COUNT = 4; + + private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat( + this::onInAppDisplayProgressChanged); + private final MultiPropertyFactory mTaskbarInAppDisplayProgressMultiProp = + new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress, + AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); + + private final QuickstepLauncher mLauncher; private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = - this::onStashedInAppChanged; - - // Initialized in init. - private AnimatedFloat mTaskbarOverrideBackgroundAlpha; - private TaskbarKeyguardController mKeyguardController; - private final TaskbarLauncherStateController - mTaskbarLauncherStateController = new TaskbarLauncherStateController(); - - private final DeviceProfile.OnDeviceProfileChangeListener mProfileChangeListener = - new DeviceProfile.OnDeviceProfileChangeListener() { - @Override - public void onDeviceProfileChanged(DeviceProfile dp) { - mControllers.taskbarViewController.onRotationChanged( - mLauncher.getDeviceProfile()); + dp -> { + onStashedInAppChanged(dp); + if (mControllers != null && mControllers.taskbarViewController != null) { + mControllers.taskbarViewController.onRotationChanged(dp); } }; - public LauncherTaskbarUIController(BaseQuickstepLauncher launcher) { + // Initialized in init. + private final TaskbarLauncherStateController + mTaskbarLauncherStateController = new TaskbarLauncherStateController(); + + public LauncherTaskbarUIController(QuickstepLauncher launcher) { mLauncher = launcher; } @@ -83,17 +97,21 @@ public class LauncherTaskbarUIController extends TaskbarUIController { super.init(taskbarControllers); mTaskbarLauncherStateController.init(mControllers, mLauncher); - mTaskbarOverrideBackgroundAlpha = mControllers.taskbarDragLayerController - .getOverrideBackgroundAlpha(); mLauncher.setTaskbarUIController(this); - mKeyguardController = taskbarControllers.taskbarKeyguardController; onLauncherResumedOrPaused(mLauncher.hasBeenResumed(), true /* fromInit */); onStashedInAppChanged(mLauncher.getDeviceProfile()); + mTaskbarLauncherStateController.updateStateForSysuiFlags( + mControllers.getSharedState().sysuiStateFlags, true /* fromInit */); mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); - mLauncher.addOnDeviceProfileChangeListener(mProfileChangeListener); + + // Restore the in-app display progress from before Taskbar was recreated. + float[] prevProgresses = mControllers.getSharedState().inAppDisplayProgressMultiPropValues; + for (int i = 0; i < prevProgresses.length; i++) { + mTaskbarInAppDisplayProgressMultiProp.get(i).setValue(prevProgresses[i]); + } } @Override @@ -102,15 +120,50 @@ public class LauncherTaskbarUIController extends TaskbarUIController { onLauncherResumedOrPaused(false); mTaskbarLauncherStateController.onDestroy(); - mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); mLauncher.setTaskbarUIController(null); - mLauncher.removeOnDeviceProfileChangeListener(mProfileChangeListener); + mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); updateTaskTransitionSpec(true); } + private void onInAppDisplayProgressChanged() { + if (mControllers != null) { + // Update our shared state so we can restore it if taskbar gets recreated. + for (int i = 0; i < DISPLAY_PROGRESS_COUNT; i++) { + mControllers.getSharedState().inAppDisplayProgressMultiPropValues[i] = + mTaskbarInAppDisplayProgressMultiProp.get(i).getValue(); + } + // Ensure nav buttons react to our latest state if necessary. + mControllers.navbarButtonsViewController.updateNavButtonTranslationY(); + } + } + @Override protected boolean isTaskbarTouchable() { - return !mTaskbarLauncherStateController.isAnimatingToLauncher(); + return !(mTaskbarLauncherStateController.isAnimatingToLauncher() + && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat()); + } + + public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) { + mTaskbarLauncherStateController.setShouldDelayLauncherStateAnim( + shouldDelayLauncherStateAnim); + } + + /** + * Adds the Launcher resume animator to the given animator set. + * + * This should be used to run a Launcher resume animation whose progress matches a + * swipe progress. + * + * @param placeholderDuration a placeholder duration to be used to ensure all full-length + * sub-animations are properly coordinated. This duration should not + * actually be used since this animation tracks a swipe progress. + */ + protected void addLauncherResumeAnimation(AnimatorSet animation, int placeholderDuration) { + animation.play(onLauncherResumedOrPaused( + /* isResumed= */ true, + /* fromInit= */ false, + /* startAnimation= */ false, + placeholderDuration)); } /** @@ -121,18 +174,29 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } private void onLauncherResumedOrPaused(boolean isResumed, boolean fromInit) { - if (mKeyguardController.isScreenOff()) { - if (!isResumed) { - return; - } else { - // Resuming implicitly means device unlocked - mKeyguardController.setScreenOn(); - } + onLauncherResumedOrPaused( + isResumed, + fromInit, + /* startAnimation= */ true, + DisplayController.isTransientTaskbar(mLauncher) + ? TRANSIENT_TASKBAR_TRANSITION_DURATION + : (!isResumed + ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION + : QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION)); + } + + @Nullable + private Animator onLauncherResumedOrPaused( + boolean isResumed, boolean fromInit, boolean startAnimation, int duration) { + if (ENABLE_SHELL_TRANSITIONS && isResumed + && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) { + // Launcher is resumed, but in a state where taskbar is still independent, so + // ignore the state change. + return null; } mTaskbarLauncherStateController.updateStateForFlag(FLAG_RESUMED, isResumed); - mTaskbarLauncherStateController.applyState( - fromInit ? 0 : QuickstepTransitionManager.CONTENT_ALPHA_DURATION); + return mTaskbarLauncherStateController.applyState(fromInit ? 0 : duration, startAnimation); } /** @@ -146,15 +210,6 @@ public class LauncherTaskbarUIController extends TaskbarUIController { return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration); } - /** - * @param ev MotionEvent in screen coordinates. - * @return Whether any Taskbar item could handle the given MotionEvent if given the chance. - */ - public boolean isEventOverAnyTaskbarItem(MotionEvent ev) { - return mControllers.taskbarViewController.isEventOverAnyItem(ev) - || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev); - } - public boolean isDraggingItem() { return mControllers.taskbarDragController.isDragging(); } @@ -177,15 +232,10 @@ public class LauncherTaskbarUIController extends TaskbarUIController { WindowManagerGlobal.getWindowManagerService().clearTaskTransitionSpec(); } else { // Adjust task transition spec to account for taskbar being visible - @ColorInt int taskAnimationBackgroundColor = - mLauncher.getColor(R.color.taskbar_background); - - TaskTransitionSpec customTaskAnimationSpec = new TaskTransitionSpec( - taskAnimationBackgroundColor, - Set.of(ITYPE_EXTRA_NAVIGATION_BAR) - ); - WindowManagerGlobal.getWindowManagerService() - .setTaskTransitionSpec(customTaskAnimationSpec); + WindowManagerGlobal.getWindowManagerService().setTaskTransitionSpec( + new TaskTransitionSpec( + mLauncher.getColor(R.color.taskbar_background), + Set.of(ITYPE_EXTRA_NAVIGATION_BAR))); } } catch (RemoteException e) { // This shouldn't happen but if it does task animations won't look good until the @@ -196,54 +246,150 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } /** - * Sets whether the background behind the taskbar/nav bar should be hidden. + * Starts a Taskbar EDU flow, if the user should see one upon launching an application. */ - public void forceHideBackground(boolean forceHide) { - mTaskbarOverrideBackgroundAlpha.updateValue(forceHide ? 0 : 1); - } - - @Override - public Stream getAppIconsForEdu() { - return Arrays.stream(mLauncher.getAppsView().getAppsStore().getApps()); - } - - /** - * Starts the taskbar education flow, if the user hasn't seen it yet. - */ - public void showEdu() { - if (!FeatureFlags.ENABLE_TASKBAR_EDU.get() - || Utilities.IS_RUNNING_IN_TEST_HARNESS - || mLauncher.getOnboardingPrefs().getBoolean(OnboardingPrefs.TASKBAR_EDU_SEEN)) { - return; - } - mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.TASKBAR_EDU_SEEN); - - mControllers.taskbarEduController.showEdu(); - } - - /** - * Manually ends the taskbar education flow. - */ - public void hideEdu() { - if (!FeatureFlags.ENABLE_TASKBAR_EDU.get()) { + public void showEduOnAppLaunch() { + if (!shouldShowEduOnAppLaunch()) { return; } - mControllers.taskbarEduController.hideEdu(); + // Transient and persistent bottom sheet. + if (!ENABLE_TASKBAR_EDU_TOOLTIP.get()) { + mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.TASKBAR_EDU_SEEN); + mControllers.taskbarEduController.showEdu(); + return; + } + + // Persistent features EDU tooltip. + if (!DisplayController.isTransientTaskbar(mLauncher)) { + mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); + return; + } + + // Transient swipe EDU tooltip. + mControllers.taskbarEduTooltipController.maybeShowSwipeEdu(); + } + + /** + * Returns {@code true} if a Taskbar education should be shown on application launch. + */ + public boolean shouldShowEduOnAppLaunch() { + if (Utilities.isRunningInTestHarness()) { + return false; + } + + // Transient and persistent bottom sheet. + if (!ENABLE_TASKBAR_EDU_TOOLTIP.get()) { + return !mLauncher.getOnboardingPrefs().getBoolean(OnboardingPrefs.TASKBAR_EDU_SEEN); + } + + // Persistent features EDU tooltip. + if (!DisplayController.isTransientTaskbar(mLauncher)) { + return !mLauncher.getOnboardingPrefs().hasReachedMaxCount( + OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP); + } + + // Transient swipe EDU tooltip. + return mControllers.taskbarEduTooltipController.getTooltipStep() < TOOLTIP_STEP_FEATURES; } @Override - public void onTaskbarIconLaunched(WorkspaceItemInfo item) { + public void onTaskbarIconLaunched(ItemInfo item) { + super.onTaskbarIconLaunched(item); InstanceId instanceId = new InstanceIdSequence().newInstanceId(); mLauncher.logAppLaunch(mControllers.taskbarActivityContext.getStatsLogManager(), item, instanceId); } + /** + * Animates Taskbar elements during a transition to a Launcher state that should use in-app + * layouts. + * + * @param progress [0, 1] + * 0 => use home layout + * 1 => use in-app layout + */ + public void onTaskbarInAppDisplayProgressUpdate(float progress, int progressIndex) { + mTaskbarInAppDisplayProgressMultiProp.get(progressIndex).setValue(progress); + if (mControllers == null) { + // This method can be called before init() is called. + return; + } + if (mControllers.uiController.isIconAlignedWithHotseat() + && !mTaskbarLauncherStateController.isAnimatingToLauncher()) { + // Only animate the nav buttons while home and not animating home, otherwise let + // the TaskbarViewController handle it. + mControllers.navbarButtonsViewController + .getTaskbarNavButtonTranslationYForInAppDisplay() + .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() + * mTaskbarInAppDisplayProgress.value); + } + } + + /** Returns true iff any in-app display progress > 0. */ + public boolean shouldUseInAppLayout() { + return mTaskbarInAppDisplayProgress.value > 0; + } + @Override - public void setSystemGestureInProgress(boolean inProgress) { - super.setSystemGestureInProgress(inProgress); - // Launcher's ScrimView will draw the background throughout the gesture. But once the - // gesture ends, start drawing taskbar's background again since launcher might stop drawing. - forceHideBackground(inProgress); + public void onExpandPip() { + super.onExpandPip(); + mTaskbarLauncherStateController.updateStateForFlag(FLAG_RESUMED, false); + mTaskbarLauncherStateController.applyState(); + } + + @Override + public void updateStateForSysuiFlags(int sysuiFlags, boolean skipAnim) { + mTaskbarLauncherStateController.updateStateForSysuiFlags(sysuiFlags, skipAnim); + } + + @Override + public boolean isIconAlignedWithHotseat() { + return mTaskbarLauncherStateController.isIconAlignedWithHotseat(); + } + + @Override + public boolean isHotseatIconOnTopWhenAligned() { + return mTaskbarLauncherStateController.isInHotseatOnTopStates() + && mTaskbarInAppDisplayProgressMultiProp.get(MINUS_ONE_PAGE_PROGRESS_INDEX) + .getValue() == 0; + } + + @Override + protected boolean isInOverview() { + return mTaskbarLauncherStateController.isInOverview(); + } + + @Override + public RecentsView getRecentsView() { + return mLauncher.getOverviewPanel(); + } + + @Override + public void launchSplitTasks(@NonNull View taskView, @NonNull GroupTask groupTask) { + mLauncher.launchSplitTasks(taskView, groupTask); + } + + @Override + protected void onIconLayoutBoundsChanged() { + mTaskbarLauncherStateController.resetIconAlignment(); + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + super.dumpLogs(prefix, pw); + + pw.println(String.format("%s\tTaskbar in-app display progress: %.2f", prefix, + mTaskbarInAppDisplayProgress.value)); + mTaskbarInAppDisplayProgressMultiProp.dump( + prefix + "\t\t", + pw, + "mTaskbarInAppDisplayProgressMultiProp", + "MINUS_ONE_PAGE_PROGRESS_INDEX", + "ALL_APPS_PAGE_PROGRESS_INDEX", + "WIDGETS_PAGE_PROGRESS_INDEX", + "SYSUI_SURFACE_PROGRESS_INDEX"); + + mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 50637a12ce..a713ff53a7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -15,13 +15,25 @@ */ package com.android.launcher3.taskbar; +import static android.view.View.AccessibilityDelegate; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; + +import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; +import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; +import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX; +import static com.android.launcher3.taskbar.TaskbarManager.isPhoneButtonNavMode; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_KEYGUARD; +import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_SMALL_SCREEN; +import static com.android.launcher3.util.FlagDebugUtils.appendFlag; +import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; @@ -32,6 +44,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N 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; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; @@ -40,43 +53,63 @@ import android.annotation.IdRes; import android.annotation.LayoutRes; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; -import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Color; +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.PaintDrawable; +import android.graphics.drawable.RotateDrawable; +import android.inputmethodservice.InputMethodService; +import android.os.Handler; import android.util.Property; 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.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; 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.navbutton.NavButtonLayoutFactory; +import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter; +import com.android.launcher3.util.DimensionUtils; +import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiValueAlpha; -import com.android.quickstep.AnimatedFloat; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; 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.system.QuickStepContract; +import java.io.PrintWriter; import java.util.ArrayList; +import java.util.StringJoiner; import java.util.function.IntPredicate; /** * Controller for managing nav bar buttons in taskbar */ -public class NavbarButtonsViewController { +public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController { private final Rect mTempRect = new Rect(); - private static final int FLAG_SWITCHER_SUPPORTED = 1 << 0; + private static final int FLAG_SWITCHER_SHOWING = 1 << 0; private static final int FLAG_IME_VISIBLE = 1 << 1; private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2; private static final int FLAG_A11Y_VISIBLE = 1 << 3; @@ -88,8 +121,22 @@ public class NavbarButtonsViewController { private static final int FLAG_DISABLE_BACK = 1 << 9; private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10; private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 11; + private static final int FLAG_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 12; + private static final int FLAG_SMALL_SCREEN = 1 << 13; + private static final int FLAG_SLIDE_IN_VIEW_VISIBLE = 1 << 14; - private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE; + /** + * Flags where a UI could be over Taskbar surfaces, so the color override should be disabled. + */ + private static final int FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED = + FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_VOICE_INTERACTION_WINDOW_SHOWING; + + private static final String NAV_BUTTONS_SEPARATE_WINDOW_TITLE = "Taskbar Nav Buttons"; + + public static final int ALPHA_INDEX_IMMERSIVE_MODE = 0; + public static final int ALPHA_INDEX_KEYGUARD_OR_DISABLE = 1; + public static final int ALPHA_INDEX_SUW = 2; + private static final int NUM_ALPHA_CHANNELS = 3; private final ArrayList mPropertyHolders = new ArrayList<>(); private final ArrayList mAllButtons = new ArrayList<>(); @@ -97,35 +144,60 @@ public class NavbarButtonsViewController { private final TaskbarActivityContext mContext; private final FrameLayout mNavButtonsView; - private final ViewGroup mNavButtonContainer; + private final LinearLayout mNavButtonContainer; // Used for IME+A11Y buttons private final ViewGroup mEndContextualContainer; private final ViewGroup mStartContextualContainer; private final int mLightIconColor; private final int mDarkIconColor; + /** Color to use for navigation bar buttons, if they are on on a Taskbar surface background. */ + private final int mOnBackgroundIconColor; private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); + private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat( + this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); - // Only applies to mTaskbarNavButtonTranslationY - private final AnimatedFloat mNavButtonTranslationYMultiplier = new AnimatedFloat( - this::updateNavButtonTranslationY); + private float mLastSetNavButtonTranslationY; + // Used for System UI state updates that should translate the nav button for in-app display. + private final AnimatedFloat mNavButtonInAppDisplayProgressForSysui = new AnimatedFloat( + this::updateNavButtonInAppDisplayProgressForSysui); + /** Expected nav button dark intensity communicated via the framework. */ private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat( - this::updateNavButtonDarkIntensity); - private final AnimatedFloat mNavButtonDarkIntensityMultiplier = new AnimatedFloat( - this::updateNavButtonDarkIntensity); + this::updateNavButtonColor); + /** {@code 1} if the Taskbar background color is fully opaque. */ + private final AnimatedFloat mOnTaskbarBackgroundNavButtonColorOverride = new AnimatedFloat( + this::updateNavButtonColor); + /** {@code 1} if a Taskbar slide in overlay is visible over Taskbar. */ + private final AnimatedFloat mSlideInViewVisibleNavButtonColorOverride = new AnimatedFloat( + this::updateNavButtonColor); + /** Disables the {@link #mOnBackgroundIconColor} override if {@code 0}. */ + private final AnimatedFloat mOnBackgroundNavButtonColorOverrideMultiplier = new AnimatedFloat( + this::updateNavButtonColor); private final RotationButtonListener mRotationButtonListener = new RotationButtonListener(); private final Rect mFloatingRotationButtonBounds = new Rect(); // Initialized in init. private TaskbarControllers mControllers; - private View mA11yButton; + private boolean mIsImeRenderingNavButtons; + private ImageView mA11yButton; private int mSysuiStateFlags; - private View mBackButton; + private ImageView mBackButton; + private ImageView mHomeButton; + private MultiValueAlpha mBackButtonAlpha; + private MultiValueAlpha mHomeButtonAlpha; private FloatingRotationButton mFloatingRotationButton; + // Variables for moving nav buttons to a separate window above IME + private boolean mAreNavButtonsInSeparateWindow = false; + private BaseDragLayer mSeparateWindowParent; // Initialized in init. + private final ViewTreeObserver.OnComputeInternalInsetsListener mSeparateWindowInsetsComputer = + this::onComputeInsetsForSeparateWindow; + private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender(); + private ImageView mRecentsButton; + public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) { mContext = context; mNavButtonsView = navButtonsView; @@ -135,6 +207,7 @@ public class NavbarButtonsViewController { mLightIconColor = context.getColor(R.color.taskbar_nav_icon_light_color); mDarkIconColor = context.getColor(R.color.taskbar_nav_icon_dark_color); + mOnBackgroundIconColor = Utilities.isDarkTheme(context) ? mLightIconColor : mDarkIconColor; } /** @@ -142,76 +215,83 @@ public class NavbarButtonsViewController { */ public void init(TaskbarControllers controllers) { mControllers = controllers; - mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarSize; - mNavButtonTranslationYMultiplier.value = 1; - boolean isThreeButtonNav = mContext.isThreeButtonNav(); - // IME switcher - View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH, - isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, - mControllers.navButtonController, R.id.ime_switcher); - mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton, - flags -> ((flags & MASK_IME_SWITCHER_VISIBLE) == MASK_IME_SWITCHER_VISIBLE) - && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0))); + DeviceProfile deviceProfile = mContext.getDeviceProfile(); + Resources resources = mContext.getResources(); + Point p = !mContext.isUserSetupComplete() + ? new Point(0, controllers.taskbarActivityContext.getSetupWindowHeight()) + : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, + TaskbarManager.isPhoneMode(deviceProfile)); + mNavButtonsView.getLayoutParams().height = p.y; + + mIsImeRenderingNavButtons = + InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar(); + if (!mIsImeRenderingNavButtons) { + // IME switcher + View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH, + isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, + mControllers.navButtonController, R.id.ime_switcher); + mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton, + flags -> ((flags & FLAG_SWITCHER_SHOWING) != 0) + && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0))); + } mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarViewController.getTaskbarIconAlpha() - .getProperty(ALPHA_INDEX_KEYGUARD), + .get(ALPHA_INDEX_KEYGUARD), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 - && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0, - MultiValueAlpha.VALUE, 1, 0)); + && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0)); + + mPropertyHolders.add(new StatePropertyHolder( + mControllers.taskbarViewController.getTaskbarIconAlpha() + .get(ALPHA_INDEX_SMALL_SCREEN), + flags -> (flags & FLAG_SMALL_SCREEN) == 0)); mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController - .getKeyguardBgTaskbar(), - flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, AnimatedFloat.VALUE, 1, 0)); + .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); // Force nav buttons (specifically back button) to be visible during setup wizard. boolean isInSetup = !mContext.isUserSetupComplete(); + boolean isInKidsMode = mContext.isNavBarKidsModeActive(); boolean alwaysShowButtons = isThreeButtonNav || isInSetup; - // Make sure to remove nav bar buttons translation when notification shade is expanded or - // IME is showing (add separate translation for IME). - int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE; - mPropertyHolders.add(new StatePropertyHolder(mNavButtonTranslationYMultiplier, + // Make sure to remove nav bar buttons translation when any of the following occur: + // - Notification shade is expanded + // - IME is showing (add separate translation for IME) + // - VoiceInteractionWindow (assistant) is showing + int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE + | FLAG_VOICE_INTERACTION_WINDOW_SHOWING; + mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, - 0, 1)); + 1, 0)); // Center nav buttons in new height for IME. - float transForIme = (mContext.getDeviceProfile().taskbarSize - - mContext.getTaskbarHeightForIme()) / 2f; + 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, AnimatedFloat.VALUE, transForIme, - defaultButtonTransY)); + 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( + mSlideInViewVisibleNavButtonColorOverride, + flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); if (alwaysShowButtons) { initButtons(mNavButtonContainer, mEndContextualContainer, mControllers.navButtonController); + updateButtonLayoutSpacing(); + updateStateForFlag(FLAG_SMALL_SCREEN, isPhoneButtonNavMode(mContext)); - if (isInSetup) { - // Since setup wizard only has back button enabled, it looks strange to be - // end-aligned, so start-align instead. - FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) - mNavButtonContainer.getLayoutParams(); - navButtonsLayoutParams.setMarginStart(navButtonsLayoutParams.getMarginEnd()); - navButtonsLayoutParams.setMarginEnd(0); - navButtonsLayoutParams.gravity = Gravity.START; - mNavButtonContainer.requestLayout(); - - // TODO(b/210906568) Dark intensity is currently not propagated during setup, so set - // it based on dark theme for now. - int mode = mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK; - boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES; - mTaskbarNavButtonDarkIntensity.updateValue(isDarkTheme ? 0 : 1); - } - - // Animate taskbar background when any of these flags are enabled - int flagsToShowBg = FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE - | FLAG_NOTIFICATION_SHADE_EXPANDED; mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), - flags -> (flags & flagsToShowBg) != 0, AnimatedFloat.VALUE, 1, 0)); + flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); // Rotation button RotationButton rotationButton = new RotationButtonImpl( @@ -229,20 +309,39 @@ public class NavbarButtonsViewController { R.dimen.floating_rotation_button_taskbar_left_margin, R.dimen.floating_rotation_button_taskbar_bottom_margin, R.dimen.floating_rotation_button_diameter, - R.dimen.key_button_ripple_max_width); + R.dimen.key_button_ripple_max_width, + R.bool.floating_rotation_button_position_left); mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton, mRotationButtonListener); - View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, - mStartContextualContainer, mControllers.navButtonController, R.id.back); - imeDownButton.setRotation(Utilities.isRtl(mContext.getResources()) ? 90 : -90); - // Rotate when Ime visible - mPropertyHolders.add(new StatePropertyHolder(imeDownButton, - flags -> (flags & FLAG_IME_VISIBLE) != 0)); + if (!mIsImeRenderingNavButtons) { + View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, + mStartContextualContainer, mControllers.navButtonController, R.id.back); + imeDownButton.setRotation(Utilities.isRtl(resources) ? 90 : -90); + // Only show when IME is visible. + mPropertyHolders.add(new StatePropertyHolder(imeDownButton, + flags -> (flags & FLAG_IME_VISIBLE) != 0)); + } } applyState(); mPropertyHolders.forEach(StatePropertyHolder::endAnimation); + + // Initialize things needed to move nav buttons to separate window. + mSeparateWindowParent = new BaseDragLayer(mContext, null, 0) { + @Override + public void recreateControllers() { + mControllers = new TouchController[0]; + } + + @Override + protected boolean canFindActiveController() { + // We don't have any controllers, but we don't want any floating views such as + // folder to intercept, either. This ensures nav buttons can always be pressed. + return false; + } + }; + mSeparateWindowParent.recreateControllers(); } private void initButtons(ViewGroup navContainer, ViewGroup endContainer, @@ -250,7 +349,10 @@ public class NavbarButtonsViewController { mBackButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mNavButtonContainer, mControllers.navButtonController, R.id.back); - mPropertyHolders.add(new StatePropertyHolder(mBackButton, + mBackButtonAlpha = new MultiValueAlpha(mBackButton, NUM_ALPHA_CHANNELS); + mBackButtonAlpha.setUpdateVisibility(true); + mPropertyHolders.add(new StatePropertyHolder( + mBackButtonAlpha.get(ALPHA_INDEX_KEYGUARD_OR_DISABLE), flags -> { // Show only if not disabled, and if not on the keyguard or otherwise only when // the bouncer or a lockscreen app is showing above the keyguard @@ -260,30 +362,46 @@ public class NavbarButtonsViewController { return (flags & FLAG_DISABLE_BACK) == 0 && ((flags & FLAG_KEYGUARD_VISIBLE) == 0 || showingOnKeyguard); })); - boolean isRtl = Utilities.isRtl(mContext.getResources()); - mPropertyHolders.add(new StatePropertyHolder( - mBackButton, flags -> (flags & FLAG_IME_VISIBLE) != 0, View.ROTATION, - isRtl ? 90 : -90, 0)); + mPropertyHolders.add(new StatePropertyHolder(mBackButton, + flags -> (flags & FLAG_IME_VISIBLE) != 0, + ROTATION_DRAWABLE_PERCENT, 1f, 0f)); // Translate back button to be at end/start of other buttons for keyguard int navButtonSize = mContext.getResources().getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size); + boolean isRtl = Utilities.isRtl(mContext.getResources()); mPropertyHolders.add(new StatePropertyHolder( mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 || (flags & FLAG_KEYGUARD_VISIBLE) != 0, VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0)); - - // home and recents buttons - View homeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer, + // home button + mHomeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer, navButtonController, R.id.home); - mPropertyHolders.add(new StatePropertyHolder(homeButton, + mHomeButtonAlpha = new MultiValueAlpha(mHomeButton, NUM_ALPHA_CHANNELS); + mHomeButtonAlpha.setUpdateVisibility(true); + mPropertyHolders.add( + new StatePropertyHolder(mHomeButtonAlpha.get( + ALPHA_INDEX_KEYGUARD_OR_DISABLE), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_HOME) == 0)); - View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS, + + // Recents button + mRecentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS, navContainer, navButtonController, R.id.recent_apps); - mPropertyHolders.add(new StatePropertyHolder(recentsButton, - flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && - (flags & FLAG_DISABLE_RECENTS) == 0)); + mHitboxExtender.init(mRecentsButton, mNavButtonsView, mContext.getDeviceProfile(), + () -> { + float[] recentsCoords = new float[2]; + getDescendantCoordRelativeToAncestor(mRecentsButton, mNavButtonsView, + recentsCoords, false); + return recentsCoords; + }, new Handler()); + mRecentsButton.setOnClickListener(v -> { + navButtonController.onButtonClick(BUTTON_RECENTS, v); + mHitboxExtender.onRecentsButtonClicked(); + }); + mPropertyHolders.add(new StatePropertyHolder(mRecentsButton, + flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0 + && !mContext.isNavBarKidsModeActive())); // A11y button mA11yButton = addButton(R.drawable.ic_sysbar_accessibility_button, BUTTON_A11Y, @@ -306,22 +424,26 @@ public class NavbarButtonsViewController { | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0; boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0; + boolean isVoiceInteractionWindowShowing = + (sysUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0; // TODO(b/202218289) we're getting IME as not visible on lockscreen from system updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible); - updateStateForFlag(FLAG_SWITCHER_SUPPORTED, isImeSwitcherShowing); + updateStateForFlag(FLAG_SWITCHER_SHOWING, isImeSwitcherShowing); updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible); updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled); updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled); updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled); updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded); updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive); + updateStateForFlag(FLAG_VOICE_INTERACTION_WINDOW_SHOWING, isVoiceInteractionWindowShowing); if (mA11yButton != null) { // Only used in 3 button boolean a11yLongClickable = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0; mA11yButton.setLongClickable(a11yLongClickable); + updateButtonLayoutSpacing(); } } @@ -336,6 +458,13 @@ public class NavbarButtonsViewController { } } + /** + * @return {@code true} if A11y is showing in 3 button nav taskbar + */ + private boolean isContextualButtonShowing() { + return mContext.isThreeButtonNav() && (mState & FLAG_A11Y_VISIBLE) != 0; + } + /** * Should be called when we need to show back button for bouncer */ @@ -354,6 +483,12 @@ public class NavbarButtonsViewController { applyState(); } + /** {@code true} if a slide in view is currently visible over taskbar. */ + public void setSlideInViewVisible(boolean isSlideInViewVisible) { + updateStateForFlag(FLAG_SLIDE_IN_VIEW_VISIBLE, isSlideInViewVisible); + applyState(); + } + /** * Returns true if IME bar is visible */ @@ -378,30 +513,72 @@ public class NavbarButtonsViewController { /** * Adds the bounds corresponding to all visible buttons to provided region */ - public void addVisibleButtonsRegion(TaskbarDragLayer parent, Region outRegion) { + public void addVisibleButtonsRegion(BaseDragLayer parent, Region outRegion) { int count = mAllButtons.size(); for (int i = 0; i < count; i++) { View button = mAllButtons.get(i); if (button.getVisibility() == View.VISIBLE) { parent.getDescendantRectRelativeToSelf(button, mTempRect); + if (mHitboxExtender.extendedHitboxEnabled()) { + mTempRect.bottom += mContext.getDeviceProfile().getTaskbarOffsetY(); + } outRegion.op(mTempRect, Op.UNION); } } } + /** + * Returns multi-value alpha controller for back button. + */ + public MultiValueAlpha getBackButtonAlpha() { + return mBackButtonAlpha; + } + + /** + * Returns multi-value alpha controller for home button. + */ + public MultiValueAlpha getHomeButtonAlpha() { + return mHomeButtonAlpha; + } + + /** + * Sets the AccessibilityDelegate for the home button. + */ + public void setHomeButtonAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) { + if (mHomeButton == null) { + return; + } + mHomeButton.setAccessibilityDelegate(accessibilityDelegate); + } + + /** + * Sets the AccessibilityDelegate for the back button. + */ + public void setBackButtonAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) { + if (mBackButton == null) { + return; + } + mBackButton.setAccessibilityDelegate(accessibilityDelegate); + } + /** Use to set the translationY for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonTranslationY() { return mTaskbarNavButtonTranslationY; } + /** Use to set the translationY for the all nav+contextual buttons when in Launcher */ + public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() { + return mTaskbarNavButtonTranslationYForInAppDisplay; + } + /** Use to set the dark intensity for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonDarkIntensity() { return mTaskbarNavButtonDarkIntensity; } - /** Use to determine whether to use the dark intensity requested by the underlying app */ - public AnimatedFloat getNavButtonDarkIntensityMultiplier() { - return mNavButtonDarkIntensityMultiplier; + /** Use to override the nav button color with {@link #mOnBackgroundIconColor}. */ + public AnimatedFloat getOnTaskbarBackgroundNavButtonColorOverride() { + return mOnTaskbarBackgroundNavButtonColorOverride; } /** @@ -422,24 +599,55 @@ public class NavbarButtonsViewController { } } - private void updateNavButtonTranslationY() { - float normalTranslationY = mTaskbarNavButtonTranslationY.value - * mNavButtonTranslationYMultiplier.value; - float otherTranslationY = mTaskbarNavButtonTranslationYForIme.value; - mNavButtonsView.setTranslationY(normalTranslationY + otherTranslationY); + private void updateNavButtonInAppDisplayProgressForSysui() { + TaskbarUIController uiController = mControllers.uiController; + if (uiController instanceof LauncherTaskbarUIController) { + ((LauncherTaskbarUIController) uiController).onTaskbarInAppDisplayProgressUpdate( + mNavButtonInAppDisplayProgressForSysui.value, SYSUI_SURFACE_PROGRESS_INDEX); + } } - private void updateNavButtonDarkIntensity() { - float darkIntensity = mTaskbarNavButtonDarkIntensity.value - * mNavButtonDarkIntensityMultiplier.value; - int iconColor = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, mLightIconColor, + /** + * Sets the translationY of the nav buttons based on the current device state. + */ + public void updateNavButtonTranslationY() { + if (isPhoneButtonNavMode(mContext)) { + return; + } + final float normalTranslationY = mTaskbarNavButtonTranslationY.value; + final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value; + TaskbarUIController uiController = mControllers.uiController; + final float inAppDisplayAdjustmentTranslationY = + (uiController instanceof LauncherTaskbarUIController + && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) + ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0; + + mLastSetNavButtonTranslationY = normalTranslationY + + imeAdjustmentTranslationY + + inAppDisplayAdjustmentTranslationY; + mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY); + } + + private void updateNavButtonColor() { + final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance(); + final int sysUiNavButtonIconColor = (int) argbEvaluator.evaluate( + mTaskbarNavButtonDarkIntensity.value, + mLightIconColor, mDarkIconColor); + // 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), + sysUiNavButtonIconColor, + mOnBackgroundIconColor); for (ImageView button : mAllButtons) { button.setImageTintList(ColorStateList.valueOf(iconColor)); } } - private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, + protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) { return addButton(drawableId, buttonType, parent, navButtonController, id, R.layout.taskbar_nav_button); @@ -450,9 +658,11 @@ public class NavbarButtonsViewController { @LayoutRes int layoutId) { ImageView buttonView = addButton(parent, id, layoutId); buttonView.setImageResource(drawableId); - buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType)); + buttonView.setContentDescription(parent.getContext().getString( + navButtonController.getButtonContentDescription(buttonType))); + buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType, view)); buttonView.setOnLongClickListener(view -> - navButtonController.onButtonLongClick(buttonType)); + navButtonController.onButtonLongClick(buttonType, view)); return buttonView; } @@ -473,6 +683,170 @@ public class NavbarButtonsViewController { if (mFloatingRotationButton != null) { mFloatingRotationButton.onConfigurationChanged(configChanges); } + if (!mContext.isUserSetupComplete()) { + handleSetupUi(); + } + updateButtonLayoutSpacing(); + } + + private void handleSetupUi() { + // Since setup wizard only has back button enabled, it looks strange to be + // end-aligned, so start-align instead. + FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) + mNavButtonContainer.getLayoutParams(); + Resources resources = mContext.getResources(); + DeviceProfile deviceProfile = mContext.getDeviceProfile(); + int setupMargin = resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_margin); + navButtonsLayoutParams.setMarginStart(setupMargin); + navButtonsLayoutParams.bottomMargin = !deviceProfile.isLandscape + ? 0 + : setupMargin - + (resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) / 2); + navButtonsLayoutParams.setMarginEnd(0); + navButtonsLayoutParams.gravity = Gravity.START; + mNavButtonsView.getLayoutParams().height = + mControllers.taskbarActivityContext.getSetupWindowHeight(); + mNavButtonContainer.setLayoutParams(navButtonsLayoutParams); + } + + /** + * Adds the correct spacing to 3 button nav container depending on if device is in kids mode, + * setup wizard, or normal 3 button nav. + */ + private void updateButtonLayoutSpacing() { + DeviceProfile dp = mContext.getDeviceProfile(); + Resources res = mContext.getResources(); + boolean isInSetup = !mContext.isUserSetupComplete(); + // TODO(b/244231596) we're getting the incorrect kidsMode value in small-screen + boolean isInKidsMode = mContext.isNavBarKidsModeActive(); + + if (TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) { + boolean isThreeButtonNav = mContext.isThreeButtonNav(); + + NavButtonLayoutter navButtonLayoutter = + NavButtonLayoutFactory.Companion.getUiLayoutter( + dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav, + TaskbarManager.isPhoneMode(dp)); + navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing()); + return; + } + + if (isInSetup) { + handleSetupUi(); + + // Hide back button in SUW if keyboard is showing (IME draws its own back). + mPropertyHolders.add(new StatePropertyHolder( + mBackButtonAlpha.get(ALPHA_INDEX_SUW), + flags -> (flags & FLAG_IME_VISIBLE) == 0)); + } else if (isInKidsMode) { + int iconSize = res.getDimensionPixelSize( + R.dimen.taskbar_icon_size_kids); + int buttonWidth = res.getDimensionPixelSize( + R.dimen.taskbar_nav_buttons_width_kids); + int buttonHeight = res.getDimensionPixelSize( + R.dimen.taskbar_nav_buttons_height_kids); + int buttonRadius = res.getDimensionPixelSize( + R.dimen.taskbar_nav_buttons_corner_radius_kids); + int paddingleft = (buttonWidth - iconSize) / 2; + int paddingRight = paddingleft; + int paddingTop = (buttonHeight - iconSize) / 2; + int paddingBottom = paddingTop; + + // Update icons + final RotateDrawable rotateDrawable = new RotateDrawable(); + rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back_kids)); + rotateDrawable.setFromDegrees(0f); + rotateDrawable.setToDegrees(-90f); + mBackButton.setImageDrawable(rotateDrawable); + mBackButton.setScaleType(ImageView.ScaleType.FIT_CENTER); + mBackButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom); + + mHomeButton.setImageDrawable( + mHomeButton.getContext().getDrawable(R.drawable.ic_sysbar_home_kids)); + mHomeButton.setScaleType(ImageView.ScaleType.FIT_CENTER); + mHomeButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom); + + // Home button layout + LinearLayout.LayoutParams homeLayoutparams = new LinearLayout.LayoutParams( + buttonWidth, + buttonHeight + ); + int homeButtonLeftMargin = res.getDimensionPixelSize( + R.dimen.taskbar_home_button_left_margin_kids); + homeLayoutparams.setMargins(homeButtonLeftMargin, 0, 0, 0); + mHomeButton.setLayoutParams(homeLayoutparams); + + // Back button layout + LinearLayout.LayoutParams backLayoutParams = new LinearLayout.LayoutParams( + buttonWidth, + buttonHeight + ); + int backButtonLeftMargin = res.getDimensionPixelSize( + R.dimen.taskbar_back_button_left_margin_kids); + backLayoutParams.setMargins(backButtonLeftMargin, 0, 0, 0); + mBackButton.setLayoutParams(backLayoutParams); + + // Button backgrounds + int whiteWith10PctAlpha = Color.argb(0.1f, 1, 1, 1); + PaintDrawable buttonBackground = new PaintDrawable(whiteWith10PctAlpha); + buttonBackground.setCornerRadius(buttonRadius); + mHomeButton.setBackground(buttonBackground); + mBackButton.setBackground(buttonBackground); + + // Update alignment within taskbar + FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) + mNavButtonContainer.getLayoutParams(); + navButtonsLayoutParams.setMarginStart( + navButtonsLayoutParams.getMarginEnd() / 2); + navButtonsLayoutParams.setMarginEnd(navButtonsLayoutParams.getMarginStart()); + navButtonsLayoutParams.gravity = Gravity.CENTER; + mNavButtonContainer.requestLayout(); + + mHomeButton.setOnLongClickListener(null); + } else if (mContext.isThreeButtonNav()) { + final RotateDrawable rotateDrawable = new RotateDrawable(); + rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back)); + rotateDrawable.setFromDegrees(0f); + rotateDrawable.setToDegrees(Utilities.isRtl(mContext.getResources()) ? 90f : -90f); + mBackButton.setImageDrawable(rotateDrawable); + + // Setup normal 3 button + // Add spacing after the end of the last nav button + FrameLayout.LayoutParams navButtonParams = + (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); + navButtonParams.gravity = Gravity.END; + navButtonParams.width = FrameLayout.LayoutParams.WRAP_CONTENT; + navButtonParams.height = MATCH_PARENT; + + int navMarginEnd = (int) res.getDimension(dp.inv.inlineNavButtonsEndSpacing); + int contextualWidth = mEndContextualContainer.getWidth(); + // If contextual buttons are showing, we check if the end margin is enough for the + // contextual button to be showing - if not, move the nav buttons over a smidge + if (isContextualButtonShowing() && navMarginEnd < contextualWidth) { + // Additional spacing, eat up half of space between last icon and nav button + navMarginEnd += res.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing) / 2; + } + navButtonParams.setMarginEnd(navMarginEnd); + mNavButtonContainer.setLayoutParams(navButtonParams); + + // Add the spaces in between the nav buttons + int spaceInBetween = res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween); + for (int i = 0; i < mNavButtonContainer.getChildCount(); i++) { + View navButton = mNavButtonContainer.getChildAt(i); + LinearLayout.LayoutParams buttonLayoutParams = + (LinearLayout.LayoutParams) navButton.getLayoutParams(); + buttonLayoutParams.weight = 0; + if (i == 0) { + buttonLayoutParams.setMarginEnd(spaceInBetween / 2); + } else if (i == mNavButtonContainer.getChildCount() - 1) { + buttonLayoutParams.setMarginStart(spaceInBetween / 2); + } else { + buttonLayoutParams.setMarginStart(spaceInBetween / 2); + buttonLayoutParams.setMarginEnd(spaceInBetween / 2); + } + } + } + } public void onDestroy() { @@ -481,6 +855,119 @@ public class NavbarButtonsViewController { if (mFloatingRotationButton != null) { mFloatingRotationButton.hide(); } + + moveNavButtonsBackToTaskbarWindow(); + mNavButtonContainer.removeAllViews(); + mEndContextualContainer.removeAllViews(); + mStartContextualContainer.removeAllViews(); + mAllButtons.clear(); + } + + /** + * Moves mNavButtonsView from TaskbarDragLayer to a placeholder BaseDragLayer on a new window. + */ + public void moveNavButtonsToNewWindow() { + if (mAreNavButtonsInSeparateWindow) { + return; + } + + if (mIsImeRenderingNavButtons) { + // IME is rendering the nav buttons, so we don't need to create a new layer for them. + return; + } + + mSeparateWindowParent.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + mSeparateWindowParent.getViewTreeObserver().addOnComputeInternalInsetsListener( + mSeparateWindowInsetsComputer); + } + + @Override + public void onViewDetachedFromWindow(View view) { + mSeparateWindowParent.removeOnAttachStateChangeListener(this); + mSeparateWindowParent.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mSeparateWindowInsetsComputer); + } + }); + + mAreNavButtonsInSeparateWindow = true; + mContext.getDragLayer().removeView(mNavButtonsView); + mSeparateWindowParent.addView(mNavButtonsView); + WindowManager.LayoutParams windowLayoutParams = mContext.createDefaultWindowLayoutParams( + TYPE_NAVIGATION_BAR_PANEL, NAV_BUTTONS_SEPARATE_WINDOW_TITLE); + mContext.addWindowView(mSeparateWindowParent, windowLayoutParams); + + } + + /** + * Moves mNavButtonsView from its temporary window and reattaches it to TaskbarDragLayer. + */ + public void moveNavButtonsBackToTaskbarWindow() { + if (!mAreNavButtonsInSeparateWindow) { + return; + } + + mAreNavButtonsInSeparateWindow = false; + mContext.removeWindowView(mSeparateWindowParent); + mSeparateWindowParent.removeView(mNavButtonsView); + mContext.getDragLayer().addView(mNavButtonsView); + } + + private void onComputeInsetsForSeparateWindow(ViewTreeObserver.InternalInsetsInfo insetsInfo) { + addVisibleButtonsRegion(mSeparateWindowParent, insetsInfo.touchableRegion); + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "NavbarButtonsViewController:"); + + pw.println(prefix + "\tmState=" + getStateString(mState)); + pw.println(prefix + "\tmLightIconColor=" + Integer.toHexString(mLightIconColor)); + pw.println(prefix + "\tmDarkIconColor=" + Integer.toHexString(mDarkIconColor)); + pw.println(prefix + "\tmFloatingRotationButtonBounds=" + mFloatingRotationButtonBounds); + pw.println(prefix + "\tmSysuiStateFlags=" + QuickStepContract.getSystemUiStateString( + mSysuiStateFlags)); + pw.println(prefix + "\tLast set nav button translationY=" + mLastSetNavButtonTranslationY); + pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY=" + + mTaskbarNavButtonTranslationY.value); + pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay=" + + mTaskbarNavButtonTranslationYForInAppDisplay.value); + pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme=" + + mTaskbarNavButtonTranslationYForIme.value); + } + + private static String getStateString(int flags) { + StringJoiner str = new StringJoiner("|"); + appendFlag(str, flags, FLAG_SWITCHER_SHOWING, "FLAG_SWITCHER_SHOWING"); + appendFlag(str, flags, FLAG_IME_VISIBLE, "FLAG_IME_VISIBLE"); + appendFlag(str, flags, FLAG_ROTATION_BUTTON_VISIBLE, "FLAG_ROTATION_BUTTON_VISIBLE"); + appendFlag(str, flags, FLAG_A11Y_VISIBLE, "FLAG_A11Y_VISIBLE"); + appendFlag(str, flags, FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, + "FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE"); + appendFlag(str, flags, FLAG_KEYGUARD_VISIBLE, "FLAG_KEYGUARD_VISIBLE"); + appendFlag(str, flags, FLAG_KEYGUARD_OCCLUDED, "FLAG_KEYGUARD_OCCLUDED"); + appendFlag(str, flags, FLAG_DISABLE_HOME, "FLAG_DISABLE_HOME"); + appendFlag(str, flags, FLAG_DISABLE_RECENTS, "FLAG_DISABLE_RECENTS"); + appendFlag(str, flags, FLAG_DISABLE_BACK, "FLAG_DISABLE_BACK"); + appendFlag(str, flags, FLAG_NOTIFICATION_SHADE_EXPANDED, + "FLAG_NOTIFICATION_SHADE_EXPANDED"); + appendFlag(str, flags, FLAG_SCREEN_PINNING_ACTIVE, "FLAG_SCREEN_PINNING_ACTIVE"); + appendFlag(str, flags, FLAG_VOICE_INTERACTION_WINDOW_SHOWING, + "FLAG_VOICE_INTERACTION_WINDOW_SHOWING"); + return str.toString(); + } + + public TouchController getTouchController() { + return mHitboxExtender; + } + + /** + * @param alignment 0 -> Taskbar, 1 -> Workspace + */ + public void updateTaskbarAlignment(float alignment) { + mHitboxExtender.onAnimationProgressToOverview(alignment); } private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @@ -585,6 +1072,15 @@ public class NavbarButtonsViewController { mAnimator.addListener(new AlphaUpdateListener(view)); } + StatePropertyHolder(MultiProperty alphaProperty, + IntPredicate enableCondition) { + this(alphaProperty, enableCondition, MULTI_PROPERTY_VALUE, 1, 0); + } + + StatePropertyHolder(AnimatedFloat animatedFloat, IntPredicate enableCondition) { + this(animatedFloat, enableCondition, AnimatedFloat.VALUE, 1, 0); + } + StatePropertyHolder(T target, IntPredicate enabledCondition, Property property, float enabledValue, float disabledValue) { mEnableCondition = enabledCondition; diff --git a/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java new file mode 100644 index 0000000000..4651570a60 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java @@ -0,0 +1,134 @@ +/* + * 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; + +import android.graphics.Rect; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.TouchDelegate; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.util.TouchController; + +import java.util.function.Supplier; + +/** + * Extends the Recents touch area during the taskbar to overview animation + * to give user some error room when trying to quickly double tap recents button since it moves. + * + * Listens for icon alignment as our indication for the animation. + */ +public class RecentsHitboxExtender implements TouchController { + + private static final int RECENTS_HITBOX_TIMEOUT_MS = 500; + + private View mRecentsButton; + private View mRecentsParent; + private DeviceProfile mDeviceProfile; + private Supplier mParentCoordSupplier; + private TouchDelegate mRecentsTouchDelegate; + /** + * Will be true while the animation from taskbar to overview is occurring. + * Lifecycle of this variable slightly extends past the animation by + * {@link #RECENTS_HITBOX_TIMEOUT_MS}, so can use this variable as a proxy for if + * the current hitbox is extended or not. + */ + private boolean mAnimatingFromTaskbarToOverview; + private float mLastIconAlignment; + private final Rect mRecentsHitBox = new Rect(); + private boolean mRecentsButtonClicked; + private Handler mHandler; + private final Runnable mRecentsHitboxResetRunnable = this::reset; + + public void init(View recentsButton, View recentsParent, DeviceProfile deviceProfile, + Supplier parentCoordSupplier, Handler handler) { + mRecentsButton = recentsButton; + mRecentsParent = recentsParent; + mDeviceProfile = deviceProfile; + mParentCoordSupplier = parentCoordSupplier; + mHandler = handler; + } + + public void onRecentsButtonClicked() { + mRecentsButtonClicked = true; + } + + /** + * @param progress 0 -> Taskbar, 1 -> Overview + */ + public void onAnimationProgressToOverview(float progress) { + if (progress == 1 || progress == 0) { + // Done w/ animation + mLastIconAlignment = progress; + if (mAnimatingFromTaskbarToOverview) { + if (progress == 1) { + // Finished animation to workspace, remove the touch delegate shortly + mHandler.postDelayed(mRecentsHitboxResetRunnable, RECENTS_HITBOX_TIMEOUT_MS); + return; + } else { + // Went back to taskbar, reset immediately + mHandler.removeCallbacks(mRecentsHitboxResetRunnable); + reset(); + } + } + } + + if (mAnimatingFromTaskbarToOverview) { + return; + } + + if (progress > 0 && mLastIconAlignment == 0 && mRecentsButtonClicked) { + // Starting animation, previously we were showing taskbar + mAnimatingFromTaskbarToOverview = true; + float[] recentsCoords = mParentCoordSupplier.get(); + int x = (int) recentsCoords[0]; + int y = (int) (recentsCoords[1]); + // Extend hitbox vertically by the offset amount from mDeviceProfile.getTaskbarOffsetY() + mRecentsHitBox.set(x, y, + x + mRecentsButton.getWidth(), + y + mRecentsButton.getHeight() + mDeviceProfile.getTaskbarOffsetY() + ); + mRecentsTouchDelegate = new TouchDelegate(mRecentsHitBox, mRecentsButton); + mRecentsParent.setTouchDelegate(mRecentsTouchDelegate); + } + } + + private void reset() { + mAnimatingFromTaskbarToOverview = false; + mRecentsButton.setTouchDelegate(null); + mRecentsHitBox.setEmpty(); + mRecentsButtonClicked = false; + } + + /** + * @return {@code true} if the bounds for recents touches are currently extended + */ + public boolean extendedHitboxEnabled() { + return mAnimatingFromTaskbarToOverview; + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return mRecentsTouchDelegate.onTouchEvent(ev); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return mRecentsHitBox.contains((int)ev.getX(), (int)ev.getY()); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java index 6db58397c3..5eec6a47ae 100644 --- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java @@ -69,6 +69,10 @@ public class StashedHandleView extends View { */ public void updateSampledRegion(Rect stashedHandleBounds) { getLocationOnScreen(mTmpArr); + // Translations are temporary due to animations, remove them for the purpose of determining + // the final region we want sampled. + mTmpArr[0] -= Math.round(getTranslationX()); + mTmpArr[1] -= Math.round(getTranslationY()); mSampledRegion.set(stashedHandleBounds); mSampledRegion.offset(mTmpArr[0], mTmpArr[1]); } diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java index 22ca63f963..c4255bf70f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.taskbar; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -25,23 +27,30 @@ import android.graphics.Rect; import android.view.View; import android.view.ViewOutlineProvider; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; -import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; -import com.android.quickstep.AnimatedFloat; import com.android.systemui.shared.navigationbar.RegionSamplingHelper; +import java.io.PrintWriter; + /** * Handles properties/data collection, then passes the results to our stashed handle View to render. */ -public class StashedHandleViewController { +public class StashedHandleViewController implements TaskbarControllers.LoggableTaskbarController { public static final int ALPHA_INDEX_STASHED = 0; public static final int ALPHA_INDEX_HOME_DISABLED = 1; - private static final int NUM_ALPHA_CHANNELS = 2; + public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 2; + public static final int ALPHA_INDEX_HIDDEN_WHILE_DREAMING = 3; + private static final int NUM_ALPHA_CHANNELS = 4; /** * The SharedPreferences key for whether the stashed handle region is dark. @@ -52,15 +61,16 @@ public class StashedHandleViewController { private final TaskbarActivityContext mActivity; private final SharedPreferences mPrefs; private final StashedHandleView mStashedHandleView; - private final int mStashedHandleWidth; + private int mStashedHandleWidth; private final int mStashedHandleHeight; - private final RegionSamplingHelper mRegionSamplingHelper; + private RegionSamplingHelper mRegionSamplingHelper; private final MultiValueAlpha mTaskbarStashedHandleAlpha; private final AnimatedFloat mTaskbarStashedHandleHintScale = new AnimatedFloat( this::updateStashedHandleHintScale); // Initialized in init. private TaskbarControllers mControllers; + private int mTaskbarSize; // The bounds we want to clip to in the settled state when showing the stashed handle. private final Rect mStashedHandleBounds = new Rect(); @@ -71,10 +81,17 @@ public class StashedHandleViewController { private float mStartProgressForNextRevealAnim; private boolean mWasLastRevealAnimReversed; + // States that affect whether region sampling is enabled or not + private boolean mIsStashed; + private boolean mTaskbarHidden; + + private float mTranslationYForSwipe; + private float mTranslationYForStash; + public StashedHandleViewController(TaskbarActivityContext activity, StashedHandleView stashedHandleView) { mActivity = activity; - mPrefs = Utilities.getPrefs(mActivity); + mPrefs = LauncherPrefs.getPrefs(mActivity); mStashedHandleView = stashedHandleView; mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, NUM_ALPHA_CHANNELS); mTaskbarStashedHandleAlpha.setUpdateVisibility(true); @@ -82,30 +99,28 @@ public class StashedHandleViewController { mPrefs.getBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, false), false /* animate */); final Resources resources = mActivity.getResources(); - mStashedHandleWidth = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width); mStashedHandleHeight = resources.getDimensionPixelSize( R.dimen.taskbar_stashed_handle_height); - mRegionSamplingHelper = new RegionSamplingHelper(mStashedHandleView, - new RegionSamplingHelper.SamplingCallback() { - @Override - public void onRegionDarknessChanged(boolean isRegionDark) { - mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */); - mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, - isRegionDark).apply(); - } - - @Override - public Rect getSampledRegion(View sampledView) { - return mStashedHandleView.getSampledRegion(); - } - }, Executors.UI_HELPER_EXECUTOR); } public void init(TaskbarControllers controllers) { mControllers = controllers; - mStashedHandleView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize; + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); + Resources resources = mActivity.getResources(); + if (isPhoneGestureNavMode(mActivity.getDeviceProfile())) { + mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_size); + mStashedHandleWidth = + resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen); + } else { + mTaskbarSize = deviceProfile.taskbarHeight; + mStashedHandleWidth = resources + .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width); + } + int taskbarBottomMargin = deviceProfile.taskbarBottomMargin; + mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin; - mTaskbarStashedHandleAlpha.getProperty(ALPHA_INDEX_STASHED).setValue(0); + mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue( + isPhoneGestureNavMode(deviceProfile) ? 1 : 0); mTaskbarStashedHandleHintScale.updateValue(1f); final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight(); @@ -132,13 +147,48 @@ public class StashedHandleViewController { view.setPivotX(stashedCenterX); view.setPivotY(stashedCenterY); }); + initRegionSampler(); + if (isPhoneGestureNavMode(deviceProfile)) { + onIsStashedChanged(true); + } } + /** + * Returns the stashed handle bounds. + * @param out The destination rect. + */ + public void getStashedHandleBounds(Rect out) { + out.set(mStashedHandleBounds); + } + + private void initRegionSampler() { + mRegionSamplingHelper = new RegionSamplingHelper(mStashedHandleView, + new RegionSamplingHelper.SamplingCallback() { + @Override + public void onRegionDarknessChanged(boolean isRegionDark) { + mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */); + mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, + isRegionDark).apply(); + } + + @Override + public Rect getSampledRegion(View sampledView) { + return mStashedHandleView.getSampledRegion(); + } + }, Executors.UI_HELPER_EXECUTOR); + } + + public void onDestroy() { mRegionSamplingHelper.stopAndDestroy(); + mRegionSamplingHelper = null; } - public MultiValueAlpha getStashedHandleAlpha() { + private boolean isPhoneGestureNavMode(DeviceProfile deviceProfile) { + return TaskbarManager.isPhoneMode(deviceProfile) && !mActivity.isThreeButtonNav(); + } + + public MultiPropertyFactory getStashedHandleAlpha() { return mTaskbarStashedHandleAlpha; } @@ -152,9 +202,20 @@ public class StashedHandleViewController { * morphs into the size of where the taskbar icons will be. */ public Animator createRevealAnimToIsStashed(boolean isStashed) { + Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds()); + float startRadius = mStashedHandleRadius; + + if (DisplayController.isTransientTaskbar(mActivity)) { + // Account for the full visual height of the transient taskbar. + int heightDiff = (mTaskbarSize - visualBounds.height()) / 2; + visualBounds.top -= heightDiff; + visualBounds.bottom += heightDiff; + + startRadius = visualBounds.height() / 2f; + } + final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider( - mStashedHandleRadius, mStashedHandleRadius, - mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds); + startRadius, mStashedHandleRadius, visualBounds, mStashedHandleBounds); boolean isReversed = !isStashed; boolean changingDirection = mWasLastRevealAnimReversed != isReversed; @@ -174,8 +235,10 @@ public class StashedHandleViewController { return revealAnim; } - public void onIsStashed(boolean isStashed) { - mRegionSamplingHelper.setWindowVisible(isStashed); + /** Called when taskbar is stashed or unstashed. */ + public void onIsStashedChanged(boolean isStashed) { + mIsStashed = isStashed; + updateRegionSamplingWindowVisibility(); if (isStashed) { mStashedHandleView.updateSampledRegion(mStashedHandleBounds); mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion()); @@ -189,15 +252,54 @@ public class StashedHandleViewController { mStashedHandleView.setScaleY(mTaskbarStashedHandleHintScale.value); } + /** + * Sets the translation of the stashed handle during the swipe up gesture. + */ + protected void setTranslationYForSwipe(float transY) { + mTranslationYForSwipe = transY; + updateTranslationY(); + } + + /** + * Sets the translation of the stashed handle during the spring on stash animation. + */ + protected void setTranslationYForStash(float transY) { + mTranslationYForStash = transY; + updateTranslationY(); + } + + private void updateTranslationY() { + mStashedHandleView.setTranslationY(mTranslationYForSwipe + mTranslationYForStash); + } + /** * Should be called when the home button is disabled, so we can hide this handle as well. */ public void setIsHomeButtonDisabled(boolean homeDisabled) { - mTaskbarStashedHandleAlpha.getProperty(ALPHA_INDEX_HOME_DISABLED).setValue( + mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_HOME_DISABLED).setValue( homeDisabled ? 0 : 1); } + public void updateStateForSysuiFlags(int systemUiStateFlags) { + mTaskbarHidden = (systemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0; + updateRegionSamplingWindowVisibility(); + } + + private void updateRegionSamplingWindowVisibility() { + mRegionSamplingHelper.setWindowVisible(mIsStashed && !mTaskbarHidden); + } + public boolean isStashedHandleVisible() { return mStashedHandleView.getVisibility() == View.VISIBLE; } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "StashedHandleViewController:"); + + pw.println(prefix + "\tisStashedHandleVisible=" + isStashedHandleVisible()); + pw.println(prefix + "\tmStashedHandleWidth=" + mStashedHandleWidth); + pw.println(prefix + "\tmStashedHandleHeight=" + mStashedHandleHeight); + mRegionSamplingHelper.dump(prefix, pw); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index c612a845a5..b5aafe29f0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -15,18 +15,26 @@ */ package com.android.launcher3.taskbar; -import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; +import static android.content.pm.PackageManager.FEATURE_PC; +import static android.os.Trace.TRACE_TAG_APP; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; +import static com.android.launcher3.Utilities.isRunningInTestHarness; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; -import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT; -import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR; +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.TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; +import static com.android.systemui.shared.testing.ResourceUtils.getBoolByName; import android.animation.AnimatorSet; +import android.animation.ValueAnimator; import android.app.ActivityOptions; import android.content.ActivityNotFoundException; import android.content.Context; @@ -34,17 +42,16 @@ import android.content.Intent; import android.content.pm.ActivityInfo.Config; import android.content.pm.LauncherApps; import android.content.res.Resources; -import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.hardware.display.DisplayManager; import android.os.Process; import android.os.SystemProperties; +import android.os.Trace; import android.provider.Settings; import android.util.Log; -import android.view.ContextThemeWrapper; import android.view.Display; import android.view.Gravity; -import android.view.LayoutInflater; import android.view.RoundedCorner; import android.view.View; import android.view.WindowManager; @@ -53,37 +60,61 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dot.DotInfo; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.logger.LauncherAtom; +import com.android.launcher3.logging.StatsLogManager; +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.popup.PopupContainerWithArrow; +import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag; +import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; +import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.ItemClickHandler; +import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.SettingsCache; -import com.android.launcher3.util.Themes; +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.ViewCache; import com.android.launcher3.views.ActivityContext; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; +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.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.unfold.updates.RotationChangeProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; +import java.io.PrintWriter; + /** * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements * that are used by both Launcher and Taskbar (such as Folder) to reference a generic * ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer. */ -public class TaskbarActivityContext extends ContextThemeWrapper implements ActivityContext { +public class TaskbarActivityContext extends BaseTaskbarContext { + + private static final String IME_DRAWS_IME_NAV_BAR_RES_NAME = "config_imeDrawsImeNavBar"; private static final boolean ENABLE_THREE_BUTTON_TASKBAR = SystemProperties.getBoolean("persist.debug.taskbar_three_button", false); @@ -91,56 +122,60 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ private static final String WINDOW_TITLE = "Taskbar"; - private final LayoutInflater mLayoutInflater; private final TaskbarDragLayer mDragLayer; private final TaskbarControllers mControllers; - private DeviceProfile mDeviceProfile; - private final WindowManager mWindowManager; private final @Nullable RoundedCorner mLeftCorner, mRightCorner; - private final int mTaskbarHeightForIme; + private DeviceProfile mDeviceProfile; private WindowManager.LayoutParams mWindowLayoutParams; private boolean mIsFullscreen; // The size we should return to when we call setTaskbarWindowFullscreen(false) private int mLastRequestedNonFullscreenHeight; - private final SysUINavigationMode.Mode mNavMode; + private NavigationMode mNavMode; + private final boolean mImeDrawsImeNavBar; private final ViewCache mViewCache = new ViewCache(); private final boolean mIsSafeModeEnabled; private final boolean mIsUserSetupComplete; + private final boolean mIsNavBarForceVisible; + private final boolean mIsNavBarKidsMode; + private boolean mIsDestroyed = false; // The flag to know if the window is excluded from magnification region computation. private boolean mIsExcludeFromMagnificationRegion = false; + private boolean mBindingItems = false; + private boolean mAddedWindow = false; - public TaskbarActivityContext(Context windowContext, DeviceProfile dp, + // The bounds of the taskbar items relative to TaskbarDragLayer + private final Rect mTransientTaskbarBounds = new Rect(); + + private final TaskbarShortcutMenuAccessibilityDelegate mAccessibilityDelegate; + + public TaskbarActivityContext(Context windowContext, DeviceProfile launcherDp, TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider unfoldTransitionProgressProvider) { - super(windowContext, Themes.getActivityThemeRes(windowContext)); - mDeviceProfile = dp; + super(windowContext); + final Resources resources = getResources(); - mNavMode = SysUINavigationMode.getMode(windowContext); + matchDeviceProfile(launcherDp, getResources()); + + mNavMode = DisplayController.getNavigationMode(windowContext); + mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false); mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode", () -> getPackageManager().isSafeMode()); - mIsUserSetupComplete = SettingsCache.INSTANCE.get(this).getValue( + SettingsCache settingsCache = SettingsCache.INSTANCE.get(this); + mIsUserSetupComplete = settingsCache.getValue( Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0); + mIsNavBarForceVisible = settingsCache.getValue( + Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0); + // TODO(b/244231596) For shared Taskbar window, update this value in init() instead so + // to get correct value when recreating the taskbar + mIsNavBarKidsMode = settingsCache.getValue( + Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0); - final Resources resources = getResources(); - updateIconSize(resources); - - mTaskbarHeightForIme = resources.getDimensionPixelSize(R.dimen.taskbar_ime_size); - - mLayoutInflater = LayoutInflater.from(this).cloneInContext(this); - - // Inflate views. - mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate( - R.layout.taskbar, null, false); - TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view); - TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim); - FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view); - StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle); - + // Get display and corners first, as views might use them in constructor. Display display = windowContext.getDisplay(); Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY ? windowContext.getApplicationContext() @@ -149,11 +184,27 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mLeftCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); mRightCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); + // Inflate views. + int taskbarLayout = DisplayController.isTransientTaskbar(this) + ? 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); + FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view); + StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle); + + mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this); + + final boolean isDesktopMode = getPackageManager().hasSystemFeature(FEATURE_PC); + // Construct controllers. mControllers = new TaskbarControllers(this, new TaskbarDragController(this), buttonController, - new NavbarButtonsViewController(this, navButtonsView), + isDesktopMode + ? new DesktopNavbarButtonsViewController(this, navButtonsView) + : new NavbarButtonsViewController(this, navButtonsView), new RotationButtonController(this, c.getColor(R.color.taskbar_nav_icon_light_color), c.getColor(R.color.taskbar_nav_icon_dark_color), @@ -165,71 +216,176 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ new TaskbarDragLayerController(this, mDragLayer), new TaskbarViewController(this, taskbarView), new TaskbarScrimViewController(this, taskbarScrimView), - new TaskbarUnfoldAnimationController(unfoldTransitionProgressProvider, - mWindowManager), + new TaskbarUnfoldAnimationController(this, unfoldTransitionProgressProvider, + mWindowManager, + new RotationChangeProvider(c.getSystemService(DisplayManager.class), this, + getMainThreadHandler())), new TaskbarKeyguardController(this), new StashedHandleViewController(this, stashedHandleView), new TaskbarStashController(this), new TaskbarEduController(this), new TaskbarAutohideSuspendController(this), - new TaskbarPopupController()); + new TaskbarPopupController(this), + new TaskbarForceVisibleImmersiveController(this), + new TaskbarOverlayController(this, launcherDp), + new TaskbarAllAppsController(), + new TaskbarInsetsController(this), + new VoiceInteractionWindowController(this), + new TaskbarTranslationController(this), + new TaskbarSpringOnStashController(this), + isDesktopMode + ? new DesktopTaskbarRecentAppsController(this) + : TaskbarRecentAppsController.DEFAULT, + new TaskbarEduTooltipController(this), + new KeyboardQuickSwitchController()); } - public void init(TaskbarSharedState sharedState) { + public void init(@NonNull TaskbarSharedState sharedState) { mLastRequestedNonFullscreenHeight = getDefaultTaskbarWindowHeight(); - mWindowLayoutParams = new WindowManager.LayoutParams( - MATCH_PARENT, - mLastRequestedNonFullscreenHeight, - TYPE_NAVIGATION_BAR_PANEL, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_SLIPPERY, - PixelFormat.TRANSLUCENT); - mWindowLayoutParams.setTitle(WINDOW_TITLE); - mWindowLayoutParams.packageName = getPackageName(); - mWindowLayoutParams.gravity = Gravity.BOTTOM; - mWindowLayoutParams.setFitInsetsTypes(0); - mWindowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; - mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mWindowLayoutParams.privateFlags = - WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; - - WindowManagerWrapper wmWrapper = WindowManagerWrapper.getInstance(); - wmWrapper.setProvidesInsetsTypes( - mWindowLayoutParams, - new int[] { ITYPE_EXTRA_NAVIGATION_BAR, ITYPE_BOTTOM_TAPPABLE_ELEMENT, - ITYPE_BOTTOM_MANDATORY_GESTURES } - ); - // Adjust the frame by the rounded corners (ie. leaving just the bar as the inset) when - // the IME is showing - mWindowLayoutParams.providedInternalImeInsets = Insets.of(0, - getDefaultTaskbarWindowHeight() - mTaskbarHeightForIme, 0, 0); + mWindowLayoutParams = + createDefaultWindowLayoutParams(TYPE_NAVIGATION_BAR_PANEL, WINDOW_TITLE); // Initialize controllers after all are constructed. mControllers.init(sharedState); updateSysuiStateFlags(sharedState.sysuiStateFlags, true /* fromInit */); + disableNavBarElements(sharedState.disableNavBarDisplayId, sharedState.disableNavBarState1, + sharedState.disableNavBarState2, false /* animate */); + onSystemBarAttributesChanged(sharedState.systemBarAttrsDisplayId, + sharedState.systemBarAttrsBehavior); + onNavButtonsDarkIntensityChanged(sharedState.navButtonsDarkIntensity); - mWindowManager.addView(mDragLayer, mWindowLayoutParams); + + if (!mAddedWindow) { + mWindowManager.addView(mDragLayer, mWindowLayoutParams); + mAddedWindow = true; + } else { + mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); + } } - /** Updates the Device profile instance to the latest representation of the screen. */ - public void updateDeviceProfile(DeviceProfile dp) { - mDeviceProfile = dp; - updateIconSize(getResources()); + /** + * Show Taskbar upon receiving broadcast + */ + public void showTaskbarFromBroadcast() { + mControllers.taskbarStashController.showTaskbarFromBroadcast(); } - private void updateIconSize(Resources resources) { - float taskbarIconSize = resources.getDimension(R.dimen.taskbar_icon_size); - mDeviceProfile.updateIconSize(1, resources); - float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx; - mDeviceProfile.updateIconSize(iconScale, resources); + @Override + public DeviceProfile getDeviceProfile() { + return mDeviceProfile; + } + + /** Updates {@link DeviceProfile} instances for any Taskbar windows. */ + public void updateDeviceProfile(DeviceProfile launcherDp, NavigationMode navMode) { + mNavMode = navMode; + mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp); + matchDeviceProfile(launcherDp, getResources()); + + AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE); + // Reapply fullscreen to take potential new screen size into account. + setTaskbarWindowFullscreen(mIsFullscreen); + + dispatchDeviceProfileChanged(); + } + + @Override + public void dispatchDeviceProfileChanged() { + super.dispatchDeviceProfileChanged(); + Trace.instantForTrack(TRACE_TAG_APP, "TaskbarActivityContext#DeviceProfileChanged", + getDeviceProfile().toSmallString()); + } + + /** + * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update + * the icon size + */ + private void matchDeviceProfile(DeviceProfile originDeviceProfile, Resources resources) { + mDeviceProfile = originDeviceProfile.toBuilder(this) + .withDimensionsOverride(deviceProfile -> { + // Taskbar should match the number of icons of hotseat + deviceProfile.numShownHotseatIcons = originDeviceProfile.numShownHotseatIcons; + // Same QSB width to have a smooth animation + deviceProfile.hotseatQsbWidth = originDeviceProfile.hotseatQsbWidth; + + // Update icon size + deviceProfile.iconSizePx = deviceProfile.taskbarIconSize; + deviceProfile.updateIconSize(1f, resources); + }).build(); + } + + /** + * Returns the View bounds of transient taskbar. + */ + public Rect getTransientTaskbarBounds() { + return mTransientTaskbarBounds; + } + + @Override + public StatsLogManager getStatsLogManager() { + // Used to mock, can't mock a default interface method directly + return super.getStatsLogManager(); + } + + /** + * Creates LayoutParams for adding a view directly to WindowManager as a new window. + * @param type The window type to pass to the created WindowManager.LayoutParams. + * @param title The window title to pass to the created WindowManager.LayoutParams. + */ + public WindowManager.LayoutParams createDefaultWindowLayoutParams(int type, String title) { + DeviceProfile deviceProfile = getDeviceProfile(); + // Taskbar is on the logical bottom of the screen + boolean isVerticalBarLayout = TaskbarManager.isPhoneMode(deviceProfile) && + deviceProfile.isLandscape; + + int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_SLIPPERY + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; + if (DisplayController.isTransientTaskbar(this) && !isRunningInTestHarness()) { + windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + } + WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams( + isVerticalBarLayout ? mLastRequestedNonFullscreenHeight : MATCH_PARENT, + isVerticalBarLayout ? MATCH_PARENT : mLastRequestedNonFullscreenHeight, + type, + windowFlags, + PixelFormat.TRANSLUCENT); + windowLayoutParams.setTitle(title); + windowLayoutParams.packageName = getPackageName(); + windowLayoutParams.gravity = !isVerticalBarLayout ? + Gravity.BOTTOM : + Gravity.END; // TODO(b/230394142): seascape + + windowLayoutParams.setFitInsetsTypes(0); + windowLayoutParams.receiveInsetsIgnoringZOrder = true; + windowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; + windowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + windowLayoutParams.privateFlags = + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + windowLayoutParams.accessibilityTitle = getString( + TaskbarManager.isPhoneMode(mDeviceProfile) + ? R.string.taskbar_phone_a11y_title + : R.string.taskbar_a11y_title); + return windowLayoutParams; } public void onConfigurationChanged(@Config int configChanges) { mControllers.onConfigurationChanged(configChanges); + if (!mIsUserSetupComplete) { + setTaskbarWindowHeight(getSetupWindowHeight()); + } } public boolean isThreeButtonNav() { - return mNavMode == Mode.THREE_BUTTONS; + return mNavMode == NavigationMode.THREE_BUTTONS; + } + + public boolean isGestureNav() { + return mNavMode == NavigationMode.NO_BUTTON; + } + + public boolean imeDrawsImeNavBar() { + return mImeDrawsImeNavBar; } public int getLeftCornerRadius() { @@ -240,9 +396,8 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ return mRightCorner == null ? 0 : mRightCorner.getRadius(); } - @Override - public LayoutInflater getLayoutInflater() { - return mLayoutInflater; + public WindowManager.LayoutParams getWindowLayoutParams() { + return mWindowLayoutParams; } @Override @@ -250,11 +405,6 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ return mDragLayer; } - @Override - public DeviceProfile getDeviceProfile() { - return mDeviceProfile; - } - @Override public Rect getFolderBoundingBox() { return mControllers.taskbarDragLayerController.getFolderBoundingBox(); @@ -270,14 +420,6 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ return mViewCache; } - @Override - public boolean supportsIme() { - // Currently we don't support IME because we have FLAG_NOT_FOCUSABLE. We can remove that - // flag when opening a floating view that needs IME (such as Folder), but then that means - // Taskbar will be below IME and thus users can't click the back button. - return false; - } - @Override public View.OnClickListener getItemOnClickListener() { return this::onTaskbarIconClicked; @@ -293,11 +435,16 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ } LauncherAtom.ContainerInfo oldContainer = itemInfoBuilder.getContainerInfo(); + LauncherAtom.TaskBarContainer.Builder taskbarBuilder = + LauncherAtom.TaskBarContainer.newBuilder(); + if (mControllers.uiController.isInOverview()) { + taskbarBuilder.setTaskSwitcherContainer( + LauncherAtom.TaskSwitcherContainer.newBuilder()); + } + if (oldContainer.hasPredictedHotseatContainer()) { LauncherAtom.PredictedHotseatContainer predictedHotseat = oldContainer.getPredictedHotseatContainer(); - LauncherAtom.TaskBarContainer.Builder taskbarBuilder = - LauncherAtom.TaskBarContainer.newBuilder(); if (predictedHotseat.hasIndex()) { taskbarBuilder.setIndex(predictedHotseat.getIndex()); @@ -310,8 +457,6 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ .setTaskBarContainer(taskbarBuilder)); } else if (oldContainer.hasHotseat()) { LauncherAtom.HotseatContainer hotseat = oldContainer.getHotseat(); - LauncherAtom.TaskBarContainer.Builder taskbarBuilder = - LauncherAtom.TaskBarContainer.newBuilder(); if (hotseat.hasIndex()) { taskbarBuilder.setIndex(hotseat.getIndex()); @@ -323,8 +468,6 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ LauncherAtom.FolderContainer.Builder folderBuilder = oldContainer.getFolder() .toBuilder(); LauncherAtom.HotseatContainer hotseat = folderBuilder.getHotseat(); - LauncherAtom.TaskBarContainer.Builder taskbarBuilder = - LauncherAtom.TaskBarContainer.newBuilder(); if (hotseat.hasIndex()) { taskbarBuilder.setIndex(hotseat.getIndex()); @@ -334,6 +477,64 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ folderBuilder.clearHotseat(); itemInfoBuilder.setContainerInfo(LauncherAtom.ContainerInfo.newBuilder() .setFolder(folderBuilder)); + } else if (oldContainer.hasAllAppsContainer()) { + itemInfoBuilder.setContainerInfo(LauncherAtom.ContainerInfo.newBuilder() + .setAllAppsContainer(oldContainer.getAllAppsContainer().toBuilder() + .setTaskbarContainer(taskbarBuilder))); + } else if (oldContainer.hasPredictionContainer()) { + itemInfoBuilder.setContainerInfo(LauncherAtom.ContainerInfo.newBuilder() + .setPredictionContainer(oldContainer.getPredictionContainer().toBuilder() + .setTaskbarContainer(taskbarBuilder))); + } + } + + @Override + public DotInfo getDotInfoForItem(ItemInfo info) { + return getPopupDataProvider().getDotInfoForItem(info); + } + + @NonNull + @Override + public PopupDataProvider getPopupDataProvider() { + return mControllers.taskbarPopupController.getPopupDataProvider(); + } + + @Override + public View.AccessibilityDelegate getAccessibilityDelegate() { + return mAccessibilityDelegate; + } + + @Override + public boolean isBindingItems() { + return mBindingItems; + } + + public void setBindingItems(boolean bindingItems) { + mBindingItems = bindingItems; + } + + @Override + public void onDragStart() { + setTaskbarWindowFullscreen(true); + } + + @Override + public void onDragEnd() { + onDragEndOrViewRemoved(); + } + + @Override + public void onPopupVisibilityChanged(boolean isVisible) { + setTaskbarWindowFocusable(isVisible); + } + + @Override + public void onSplitScreenMenuButtonClicked() { + PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(this); + if (popup != null) { + popup.addOnCloseCallback(() -> { + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); + }); } } @@ -360,26 +561,38 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mIsDestroyed = true; setUIController(TaskbarUIController.DEFAULT); mControllers.onDestroy(); - mWindowManager.removeViewImmediate(mDragLayer); + if (!FLAG_HIDE_NAVBAR_WINDOW) { + mWindowManager.removeViewImmediate(mDragLayer); + mAddedWindow = false; + } + } + + public boolean isDestroyed() { + return mIsDestroyed; } public void updateSysuiStateFlags(int systemUiStateFlags, boolean fromInit) { mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); - mControllers.taskbarViewController.setImeIsVisible( - mControllers.navbarButtonsViewController.isImeVisible()); - int shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED - | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; - onNotificationShadeExpandChanged((systemUiStateFlags & shadeExpandedFlags) != 0, fromInit); + boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0; + onNotificationShadeExpandChanged(isShadeVisible, fromInit); mControllers.taskbarViewController.setRecentsButtonDisabled( - mControllers.navbarButtonsViewController.isRecentsDisabled()); + mControllers.navbarButtonsViewController.isRecentsDisabled() + || isNavBarKidsModeActive()); mControllers.stashedHandleViewController.setIsHomeButtonDisabled( mControllers.navbarButtonsViewController.isHomeDisabled()); + mControllers.stashedHandleViewController.updateStateForSysuiFlags(systemUiStateFlags); mControllers.taskbarKeyguardController.updateStateForSysuiFlags(systemUiStateFlags); - mControllers.taskbarStashController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); + mControllers.taskbarStashController.updateStateForSysuiFlags( + systemUiStateFlags, fromInit || !isUserSetupComplete()); mControllers.taskbarScrimViewController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); mControllers.navButtonController.updateSysuiFlags(systemUiStateFlags); + mControllers.taskbarForceVisibleImmersiveController.updateSysuiFlags(systemUiStateFlags); + mControllers.voiceInteractionWindowController.setIsVoiceInteractionWindowVisible( + (systemUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0, fromInit); + + mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); } /** @@ -388,12 +601,10 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ private void onNotificationShadeExpandChanged(boolean isExpanded, boolean skipAnim) { float alpha = isExpanded ? 0 : 1; AnimatorSet anim = new AnimatorSet(); - anim.play(mControllers.taskbarViewController.getTaskbarIconAlpha().getProperty( + anim.play(mControllers.taskbarViewController.getTaskbarIconAlpha().get( TaskbarViewController.ALPHA_INDEX_NOTIFICATION_EXPANDED).animateToValue(alpha)); - if (!isThreeButtonNav()) { - anim.play(mControllers.taskbarDragLayerController.getNotificationShadeBgTaskbar() - .animateToValue(alpha)); - } + anim.play(mControllers.taskbarDragLayerController.getNotificationShadeBgTaskbar() + .animateToValue(alpha)); anim.start(); if (skipAnim) { anim.end(); @@ -416,27 +627,56 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ } public void onNavButtonsDarkIntensityChanged(float darkIntensity) { - if (!isUserSetupComplete()) { - return; - } mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity() .updateValue(darkIntensity); } + /** + * Called to update a {@link AutohideSuspendFlag} with a new value. + */ + public void setAutohideSuspendFlag(@AutohideSuspendFlag int flag, boolean newValue) { + mControllers.taskbarAutohideSuspendController.updateFlag(flag, newValue); + } + /** * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size. */ public void setTaskbarWindowFullscreen(boolean fullscreen) { - mControllers.taskbarAutohideSuspendController.updateFlag( - TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen); + setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen); mIsFullscreen = fullscreen; setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenHeight); } + /** + * Called when drag ends or when a view is removed from the DragLayer. + */ + void onDragEndOrViewRemoved() { + boolean isDragInProgress = mControllers.taskbarDragController.isSystemDragInProgress(); + + if (!isDragInProgress && !AbstractFloatingView.hasOpenView(this, TYPE_ALL)) { + // Reverts Taskbar window to its original size + setTaskbarWindowFullscreen(false); + } + + setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, isDragInProgress); + } + public boolean isTaskbarWindowFullscreen() { return mIsFullscreen; } + /** + * Notify system to inset the rounded corner frame based on the task bar insets. + */ + public void updateInsetRoundedCornerFrame(boolean shouldInsetsRoundedCorner) { + if (!mDragLayer.isAttachedToWindow() + || mWindowLayoutParams.insetsRoundedCornerFrame == shouldInsetsRoundedCorner) { + return; + } + mWindowLayoutParams.insetsRoundedCornerFrame = shouldInsetsRoundedCorner; + mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); + } + /** * Updates the TaskbarContainer height (pass {@link #getDefaultTaskbarWindowHeight()} to reset). */ @@ -457,8 +697,7 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ } } mWindowLayoutParams.height = height; - mWindowLayoutParams.providedInternalImeInsets = - Insets.of(0, height - mTaskbarHeightForIme, 0, 0); + mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged(); mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); } @@ -466,14 +705,75 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ * Returns the default height of the window, including the static corner radii above taskbar. */ public int getDefaultTaskbarWindowHeight() { - return mDeviceProfile.taskbarSize + Math.max(getLeftCornerRadius(), getRightCornerRadius()); + Resources resources = getResources(); + + if (FLAG_HIDE_NAVBAR_WINDOW && mDeviceProfile.isPhone) { + return isThreeButtonNav() ? + resources.getDimensionPixelSize(R.dimen.taskbar_size) : + resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); + } + + if (!isUserSetupComplete()) { + return getSetupWindowHeight(); + } + + if (DisplayController.isTransientTaskbar(this)) { + return mDeviceProfile.taskbarHeight + + (2 * mDeviceProfile.taskbarBottomMargin) + + resources.getDimensionPixelSize(R.dimen.transient_taskbar_shadow_blur); + } + + return mDeviceProfile.taskbarHeight + + Math.max(getLeftCornerRadius(), getRightCornerRadius()); + } + + public int getSetupWindowHeight() { + return getResources().getDimensionPixelSize(R.dimen.taskbar_suw_frame); } /** - * Returns the bottom insets taskbar provides to the IME when IME is visible. + * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar + * window. */ - public int getTaskbarHeightForIme() { - return mTaskbarHeightForIme; + public void setTaskbarWindowFocusable(boolean focusable) { + if (focusable) { + mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; + } + mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); + } + + /** + * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar + * window. If we're now focusable, also move nav buttons to a separate window above IME. + */ + public void setTaskbarWindowFocusableForIme(boolean focusable) { + if (focusable) { + mControllers.navbarButtonsViewController.moveNavButtonsToNewWindow(); + } else { + mControllers.navbarButtonsViewController.moveNavButtonsBackToTaskbarWindow(); + } + setTaskbarWindowFocusable(focusable); + } + + /** Adds the given view to WindowManager with the provided LayoutParams (creates new window). */ + public void addWindowView(View view, WindowManager.LayoutParams windowLayoutParams) { + if (!view.isAttachedToWindow()) { + mWindowManager.addView(view, windowLayoutParams); + } + } + + /** Removes the given view from WindowManager. See {@link #addWindowView}. */ + public void removeWindowView(View view) { + if (view.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(view); + } + } + + @Override + public void startSplitSelection(SplitSelectSource splitSelectSource) { + mControllers.uiController.startSplitSelection(splitSelectSource); } protected void onTaskbarIconClicked(View view) { @@ -482,9 +782,21 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ Task task = (Task) tag; ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key, ActivityOptions.makeBasic()); + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } else if (tag instanceof FolderInfo) { FolderIcon folderIcon = (FolderIcon) view; Folder folder = folderIcon.getFolder(); + + folder.setOnFolderStateChangedListener(newState -> { + if (newState == Folder.STATE_OPEN) { + setTaskbarWindowFocusableForIme(true); + } else if (newState == Folder.STATE_CLOSED) { + // Defer by a frame to ensure we're no longer fullscreen and thus won't jump. + getDragLayer().post(() -> setTaskbarWindowFocusableForIme(false)); + folder.setOnFolderStateChangedListener(null); + } + }); + setTaskbarWindowFullscreen(true); getDragLayer().post(() -> { @@ -500,41 +812,70 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ }); }); } else if (tag instanceof WorkspaceItemInfo) { + // Tapping a launchable icon on Taskbar WorkspaceItemInfo info = (WorkspaceItemInfo) tag; - if (info.isDisabled()) { - ItemClickHandler.handleDisabledItemClicked(info, this); - } else { - Intent intent = new Intent(info.getIntent()) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) { - Toast.makeText(this, R.string.safemode_shortcut_error, - Toast.LENGTH_SHORT).show(); - } else if (info.isPromise()) { - intent = new PackageManagerHelper(this) - .getMarketIntent(info.getTargetPackage()) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); + if (!info.isDisabled() || !ItemClickHandler.handleDisabledItemClicked(info, this)) { + TaskbarUIController taskbarUIController = mControllers.uiController; + RecentsView recents = taskbarUIController.getRecentsView(); + if (recents != null && recents.isSplitSelectionActive()) { + // If we are selecting a second app for split, launch the split tasks + taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); + } else { + // Else launch the selected task + Intent intent = new Intent(info.getIntent()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) { + Toast.makeText(this, R.string.safemode_shortcut_error, + Toast.LENGTH_SHORT).show(); + } else if (info.isPromise()) { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, "start: taskbarPromiseIcon"); + intent = new PackageManagerHelper(this) + .getMarketIntent(info.getTargetPackage()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); - } else if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - String id = info.getDeepShortcutId(); - String packageName = intent.getPackage(); - getSystemService(LauncherApps.class) - .startShortcut(packageName, id, null, null, info.user); - } else if (info.user.equals(Process.myUserHandle())) { - startActivity(intent); - } else { - getSystemService(LauncherApps.class).startMainActivity( - intent.getComponent(), info.user, intent.getSourceBounds(), null); + } else if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, "start: taskbarDeepShortcut"); + String id = info.getDeepShortcutId(); + String packageName = intent.getPackage(); + getSystemService(LauncherApps.class) + .startShortcut(packageName, id, null, null, info.user); + } else { + launchFromTaskbarPreservingSplitIfVisible(recents, info); + } + + } catch (NullPointerException + | ActivityNotFoundException + | SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) + .show(); + Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e); + return; } - mControllers.uiController.onTaskbarIconLaunched(info); - } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { - Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) - .show(); - Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e); } + mControllers.uiController.onTaskbarIconLaunched(info); + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } + } else if (tag instanceof AppInfo) { + // Tapping an item in AllApps + AppInfo info = (AppInfo) tag; + TaskbarUIController taskbarUIController = mControllers.uiController; + RecentsView recents = taskbarUIController.getRecentsView(); + if (recents != null + && taskbarUIController.getRecentsView().isSplitSelectionActive()) { + // If we are selecting a second app for split, launch the split tasks + taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); + } else { + launchFromTaskbarPreservingSplitIfVisible(recents, info); + } + mControllers.uiController.onTaskbarIconLaunched(info); + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); + } else if (tag instanceof ItemClickProxy) { + ((ItemClickProxy) tag).onItemClicked(view); } else { Log.e(TAG, "Unknown type clicked: " + tag); } @@ -542,6 +883,55 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ AbstractFloatingView.closeAllOpenViews(this); } + /** + * Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently + * visible to the user in Overview, or is part of a visible split pair, we expand the TaskView + * as if the user tapped on it (preserving the split pair). Otherwise, launch it normally + * (potentially breaking a split pair). + */ + private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents, + ItemInfo info) { + if (recents == null) { + return; + } + ComponentKey componentToBeLaunched = new ComponentKey(info.getTargetComponent(), info.user); + recents.getSplitSelectController().findLastActiveTaskAndRunCallback( + componentToBeLaunched, + foundTask -> { + if (foundTask != null) { + TaskView foundTaskView = + recents.getTaskViewByTaskId(foundTask.key.id); + if (foundTaskView != null + && foundTaskView.isVisibleToUser()) { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon"); + foundTaskView.launchTasks(); + return; + } + } + startItemInfoActivity(info); + }); + } + + private void startItemInfoActivity(ItemInfo info) { + Intent intent = new Intent(info.getIntent()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon"); + if (info.user.equals(Process.myUserHandle())) { + // TODO(b/216683257): Use startActivityForResult for search results that require it. + startActivity(intent); + } else { + getSystemService(LauncherApps.class).startMainActivity( + intent.getComponent(), info.user, intent.getSourceBounds(), null); + } + } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) + .show(); + Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e); + } + } + /** * Called when we detect a long press in the nav region before passing the gesture slop. * @return Whether taskbar handled the long press, and thus should cancel the gesture. @@ -550,6 +940,40 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ return mControllers.taskbarStashController.onLongPressToUnstashTaskbar(); } + /** + * Called when we want to unstash taskbar when user performs swipes up gesture. + */ + public void onSwipeToUnstashTaskbar() { + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false); + mControllers.taskbarEduTooltipController.hide(); + } + + /** Returns {@code true} if taskbar All Apps is open. */ + public boolean isTaskbarAllAppsOpen() { + return mControllers.taskbarAllAppsController.isOpen(); + } + + /** + * Called to start the taskbar translation spring to its settled translation (0). + */ + public void startTranslationSpring() { + mControllers.taskbarTranslationController.startSpring(); + } + + /** + * Returns a callback to help monitor the swipe gesture. + */ + public TransitionCallback getTranslationCallbacks() { + return mControllers.taskbarTranslationController.getTransitionCallback(); + } + + /** + * Called when a transient Autohide flag suspend status changes. + */ + public void onTransientAutohideSuspendFlagChanged(boolean isSuspended) { + mControllers.taskbarStashController.updateTaskbarTimeout(isSuspended); + } + /** * Called when we detect a motion down or up/cancel in the nav region while stashed. * @param animateForward Whether to animate towards the unstashed hint state or back to stashed. @@ -558,10 +982,89 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mControllers.taskbarStashController.startUnstashHint(animateForward); } + /** + * Enables manual taskbar stashing. This method should only be used for tests that need to + * stash/unstash the taskbar. + */ + @VisibleForTesting + public void enableManualStashingDuringTests(boolean enableManualStashing) { + mControllers.taskbarStashController.enableManualStashingDuringTests(enableManualStashing); + } + + /** + * Enables the auto timeout for taskbar stashing. This method should only be used for taskbar + * testing. + */ + @VisibleForTesting + public void enableBlockingTimeoutDuringTests(boolean enableBlockingTimeout) { + mControllers.taskbarStashController.enableBlockingTimeoutDuringTests(enableBlockingTimeout); + } + + /** + * Unstashes the Taskbar if it is stashed. This method should only be used to unstash the + * taskbar at the end of a test. + */ + @VisibleForTesting + public void unstashTaskbarIfStashed() { + if (DisplayController.isTransientTaskbar(this)) { + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false); + } else { + mControllers.taskbarStashController.onLongPressToUnstashTaskbar(); + } + } + protected boolean isUserSetupComplete() { return mIsUserSetupComplete; } + public boolean isNavBarKidsModeActive() { + return mIsNavBarKidsMode && isThreeButtonNav(); + } + + protected boolean isNavBarForceVisible() { + return mIsNavBarForceVisible; + } + + /** + * Displays a single frame of the Launcher start from SUW animation. + * + * This animation is a combination of the Launcher resume animation, which animates the hotseat + * icons into position, the Taskbar unstash to hotseat animation, which animates the Taskbar + * stash bar into the hotseat icons, and an override to prevent showing the Taskbar all apps + * button. + * + * This should be used to run a Taskbar unstash to hotseat animation whose progress matches a + * swipe progress. + * + * @param duration a placeholder duration to be used to ensure all full-length + * sub-animations are properly coordinated. This duration should not actually + * be used since this animation tracks a swipe progress. + */ + protected AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) { + AnimatorSet fullAnimation = new AnimatorSet(); + fullAnimation.setDuration(duration); + + TaskbarUIController uiController = mControllers.uiController; + if (uiController instanceof LauncherTaskbarUIController) { + ((LauncherTaskbarUIController) uiController).addLauncherResumeAnimation( + fullAnimation, duration); + } + mControllers.taskbarStashController.addUnstashToHotseatAnimation(fullAnimation, duration); + + View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView(); + if (allAppsButton != null && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) { + ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1); + alphaOverride.setDuration(duration); + alphaOverride.addUpdateListener(a -> { + // Override the alpha updates in the icon alignment animation. + allAppsButton.setAlpha(0); + }); + fullAnimation.play(alphaOverride); + } + + return AnimatorPlaybackController.wrap(fullAnimation, duration); + } + /** * Called when we determine the touchable region. * @@ -582,4 +1085,41 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ } mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); } + + void notifyUpdateLayoutParams() { + if (mDragLayer.isAttachedToWindow()) { + mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); + } + } + + public void showPopupMenuForIcon(BubbleTextView btv) { + setTaskbarWindowFullscreen(true); + btv.post(() -> mControllers.taskbarPopupController.showForIcon(btv)); + } + + public boolean isInApp() { + return mControllers.taskbarStashController.isInApp(); + } + + protected void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarActivityContext:"); + + pw.println(String.format( + "%s\tmNavMode=%s", prefix, mNavMode)); + pw.println(String.format( + "%s\tmImeDrawsImeNavBar=%b", prefix, mImeDrawsImeNavBar)); + pw.println(String.format( + "%s\tmIsUserSetupComplete=%b", prefix, mIsUserSetupComplete)); + pw.println(String.format( + "%s\tmWindowLayoutParams.height=%dpx", prefix, mWindowLayoutParams.height)); + pw.println(String.format( + "%s\tmBindInProgress=%b", prefix, mBindingItems)); + mControllers.dumpLogs(prefix + "\t", pw); + mDeviceProfile.dump(this, prefix, pw); + } + + @VisibleForTesting + public int getTaskbarAllAppsTopPadding() { + return mControllers.taskbarAllAppsController.getTaskbarAllAppsTopPadding(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java index e42f83de6a..2517ff671c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java @@ -15,33 +15,52 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.util.FlagDebugUtils.appendFlag; + import androidx.annotation.IntDef; import com.android.quickstep.SystemUiProxy; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.StringJoiner; /** * Normally Taskbar will auto-hide when entering immersive (fullscreen) apps. This controller allows * us to suspend that behavior in certain cases (e.g. opening a Folder or dragging an icon). */ -public class TaskbarAutohideSuspendController { +public class TaskbarAutohideSuspendController implements + TaskbarControllers.LoggableTaskbarController { + // Taskbar window is fullscreen. public static final int FLAG_AUTOHIDE_SUSPEND_FULLSCREEN = 1 << 0; + // User is dragging item. public static final int FLAG_AUTOHIDE_SUSPEND_DRAGGING = 1 << 1; + // User has touched down but has not lifted finger. + public static final int FLAG_AUTOHIDE_SUSPEND_TOUCHING = 1 << 2; + // Taskbar EDU overlay is open above the Taskbar. */ + public static final int FLAG_AUTOHIDE_SUSPEND_EDU_OPEN = 1 << 3; + // Taskbar in immersive mode in overview + public static final int FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER = 1 << 4; + @IntDef(flag = true, value = { FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, FLAG_AUTOHIDE_SUSPEND_DRAGGING, + FLAG_AUTOHIDE_SUSPEND_TOUCHING, + FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, + FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, }) @Retention(RetentionPolicy.SOURCE) public @interface AutohideSuspendFlag {} + private final TaskbarActivityContext mActivity; private final SystemUiProxy mSystemUiProxy; private @AutohideSuspendFlag int mAutohideSuspendFlags = 0; public TaskbarAutohideSuspendController(TaskbarActivityContext activity) { + mActivity = activity; mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); } @@ -53,11 +72,49 @@ public class TaskbarAutohideSuspendController { * Adds or removes the given flag, then notifies system UI proxy whether to suspend auto-hide. */ public void updateFlag(@AutohideSuspendFlag int flag, boolean enabled) { + int flagsBefore = mAutohideSuspendFlags; if (enabled) { mAutohideSuspendFlags |= flag; } else { mAutohideSuspendFlags &= ~flag; } - mSystemUiProxy.notifyTaskbarAutohideSuspend(mAutohideSuspendFlags != 0); + if (flagsBefore == mAutohideSuspendFlags) { + // Nothing has changed, no need to notify. + return; + } + + boolean isSuspended = isSuspended(); + mSystemUiProxy.notifyTaskbarAutohideSuspend(isSuspended); + mActivity.onTransientAutohideSuspendFlagChanged(isSuspended); + } + + /** + * Returns true iff taskbar autohide is currently suspended. + */ + public boolean isSuspended() { + return mAutohideSuspendFlags != 0; + } + + public boolean isSuspendedForTransientTaskbarInOverview() { + return (mAutohideSuspendFlags & FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER) != 0; + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarAutohideSuspendController:"); + + pw.println(prefix + "\tmAutohideSuspendFlags=" + getStateString(mAutohideSuspendFlags)); + } + + private static String getStateString(int flags) { + StringJoiner str = new StringJoiner("|"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, + "FLAG_AUTOHIDE_SUSPEND_FULLSCREEN"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_DRAGGING, "FLAG_AUTOHIDE_SUSPEND_DRAGGING"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TOUCHING, "FLAG_AUTOHIDE_SUSPEND_TOUCHING"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, "FLAG_AUTOHIDE_SUSPEND_EDU_OPEN"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, + "FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER"); + return str.toString(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt new file mode 100644 index 0000000000..0215a3fcf1 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt @@ -0,0 +1,178 @@ +/* + * 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 + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.Utilities.mapRange +import com.android.launcher3.Utilities.mapToRange +import com.android.launcher3.anim.Interpolators +import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound +import com.android.launcher3.util.DisplayController + +/** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */ +class TaskbarBackgroundRenderer(context: TaskbarActivityContext) { + + private val DARK_THEME_SHADOW_ALPHA = 51f + private val LIGHT_THEME_SHADOW_ALPHA = 25f + + val paint = Paint() + val lastDrawnTransientRect = RectF() + var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat() + var translationYForSwipe = 0f + var translationYForStash = 0f + + private var maxBackgroundHeight = context.deviceProfile.taskbarHeight.toFloat() + private val transientBackgroundBounds = context.transientTaskbarBounds + + private val isTransientTaskbar = DisplayController.isTransientTaskbar(context) + + private val shadowAlpha: Float + private var shadowBlur = 0f + private var keyShadowDistance = 0f + private var bottomMargin = 0 + + private val fullLeftCornerRadius = context.leftCornerRadius.toFloat() + private val fullRightCornerRadius = context.rightCornerRadius.toFloat() + private var leftCornerRadius = fullLeftCornerRadius + private var rightCornerRadius = fullRightCornerRadius + private val square: Path = Path() + private val circle: Path = Path() + private val invertedLeftCornerPath: Path = Path() + private val invertedRightCornerPath: Path = Path() + + private val stashedHandleWidth = + context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width) + + private val stashedHandleHeight = + context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height) + + init { + paint.color = context.getColor(R.color.taskbar_background) + paint.flags = Paint.ANTI_ALIAS_FLAG + paint.style = Paint.Style.FILL + + if (isTransientTaskbar) { + val res = context.resources + bottomMargin = res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) + shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur) + keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance) + } + + shadowAlpha = + if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA + else LIGHT_THEME_SHADOW_ALPHA + + setCornerRoundness(DEFAULT_ROUNDNESS) + } + + /** + * Sets the roundness of the round corner above Taskbar. No effect on transient Taskkbar. + * + * @param cornerRoundness 0 has no round corner, 1 has complete round corner. + */ + fun setCornerRoundness(cornerRoundness: Float) { + if (isTransientTaskbar && !transientBackgroundBounds.isEmpty) { + return + } + + leftCornerRadius = fullLeftCornerRadius * cornerRoundness + rightCornerRadius = fullRightCornerRadius * cornerRoundness + + // Create the paths for the inverted rounded corners above the taskbar. Start with a filled + // square, and then subtract out a circle from the appropriate corner. + square.reset() + square.addRect(0f, 0f, leftCornerRadius, leftCornerRadius, Path.Direction.CW) + circle.reset() + circle.addCircle(leftCornerRadius, 0f, leftCornerRadius, Path.Direction.CW) + invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE) + + square.reset() + square.addRect(0f, 0f, rightCornerRadius, rightCornerRadius, Path.Direction.CW) + circle.reset() + circle.addCircle(0f, 0f, rightCornerRadius, Path.Direction.CW) + invertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE) + } + + /** Draws the background with the given paint and height, on the provided canvas. */ + fun draw(canvas: Canvas) { + canvas.save() + if (!isTransientTaskbar || transientBackgroundBounds.isEmpty) { + canvas.translate(0f, canvas.height - backgroundHeight - bottomMargin) + // Draw the background behind taskbar content. + canvas.drawRect(0f, 0f, canvas.width.toFloat(), backgroundHeight, paint) + + // Draw the inverted rounded corners above the taskbar. + canvas.translate(0f, -leftCornerRadius) + canvas.drawPath(invertedLeftCornerPath, paint) + canvas.translate(0f, leftCornerRadius) + canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius) + canvas.drawPath(invertedRightCornerPath, paint) + } else { + // backgroundHeight is a value from [0...maxBackgroundHeight], so we can use it as a + // proxy to figure out the animation progress of the stash/unstash animation. + val progress = backgroundHeight / maxBackgroundHeight + + // At progress 0, we draw the background as the stashed handle. + // At progress 1, we draw the background as the full taskbar. + val newBackgroundHeight = + mapRange(progress, stashedHandleHeight.toFloat(), maxBackgroundHeight) + val fullWidth = transientBackgroundBounds.width() + val newWidth = mapRange(progress, stashedHandleWidth.toFloat(), fullWidth.toFloat()) + val halfWidthDelta = (fullWidth - newWidth) / 2f + val radius = newBackgroundHeight / 2f + val bottomMarginProgress = bottomMargin * ((1f - progress) / 2f) + + // Aligns the bottom with the bottom of the stashed handle. + val bottom = + canvas.height - bottomMargin + + bottomMarginProgress + + translationYForSwipe + + translationYForStash + + -mapRange(1f - progress, 0f, stashedHandleHeight / 2f) + + // Draw shadow. + val newShadowAlpha = + mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR) + paint.setShadowLayer( + shadowBlur, + 0f, + keyShadowDistance, + setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)) + ) + + lastDrawnTransientRect.set( + transientBackgroundBounds.left + halfWidthDelta, + bottom - newBackgroundHeight, + transientBackgroundBounds.right - halfWidthDelta, + bottom + ) + + canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, paint) + } + canvas.restore() + } + + companion object { + const val DEFAULT_ROUNDNESS = 1f + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index c43fbf9b20..a71e5db044 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -18,9 +18,15 @@ package com.android.launcher3.taskbar; import android.content.pm.ActivityInfo.Config; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; import com.android.systemui.shared.rotation.RotationButtonController; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -30,6 +36,7 @@ import java.util.List; public class TaskbarControllers { public final TaskbarActivityContext taskbarActivityContext; + public final TaskbarDragController taskbarDragController; public final TaskbarNavButtonController navButtonController; public final NavbarButtonsViewController navbarButtonsViewController; @@ -44,6 +51,19 @@ public class TaskbarControllers { public final TaskbarEduController taskbarEduController; public final TaskbarAutohideSuspendController taskbarAutohideSuspendController; public final TaskbarPopupController taskbarPopupController; + public final TaskbarForceVisibleImmersiveController taskbarForceVisibleImmersiveController; + public final TaskbarAllAppsController taskbarAllAppsController; + public final TaskbarInsetsController taskbarInsetsController; + public final VoiceInteractionWindowController voiceInteractionWindowController; + public final TaskbarRecentAppsController taskbarRecentAppsController; + public final TaskbarTranslationController taskbarTranslationController; + public final TaskbarSpringOnStashController taskbarSpringOnStashController; + public final TaskbarOverlayController taskbarOverlayController; + public final TaskbarEduTooltipController taskbarEduTooltipController; + public final KeyboardQuickSwitchController keyboardQuickSwitchController; + + @Nullable private LoggableTaskbarController[] mControllersToLog = null; + @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null; /** Do not store this controller, as it may change at runtime. */ @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT; @@ -51,6 +71,18 @@ public class TaskbarControllers { private boolean mAreAllControllersInitialized; private final List mPostInitCallbacks = new ArrayList<>(); + @Nullable private TaskbarSharedState mSharedState = null; + + // Roundness property for round corner above taskbar . + private final AnimatedFloat mCornerRoundness = new AnimatedFloat(this::updateCornerRoundness); + + /** + * Want to add a new controller? Don't forget to: + * * Call init + * * Call onDestroy + * * Add to mControllersToLog + * * Add tests by adding this controller to TaskbarBaseTestCase.kt and extending that class + */ public TaskbarControllers(TaskbarActivityContext taskbarActivityContext, TaskbarDragController taskbarDragController, TaskbarNavButtonController navButtonController, @@ -65,7 +97,17 @@ public class TaskbarControllers { TaskbarStashController taskbarStashController, TaskbarEduController taskbarEduController, TaskbarAutohideSuspendController taskbarAutoHideSuspendController, - TaskbarPopupController taskbarPopupController) { + TaskbarPopupController taskbarPopupController, + TaskbarForceVisibleImmersiveController taskbarForceVisibleImmersiveController, + TaskbarOverlayController taskbarOverlayController, + TaskbarAllAppsController taskbarAllAppsController, + TaskbarInsetsController taskbarInsetsController, + VoiceInteractionWindowController voiceInteractionWindowController, + TaskbarTranslationController taskbarTranslationController, + TaskbarSpringOnStashController taskbarSpringOnStashController, + TaskbarRecentAppsController taskbarRecentAppsController, + TaskbarEduTooltipController taskbarEduTooltipController, + KeyboardQuickSwitchController keyboardQuickSwitchController) { this.taskbarActivityContext = taskbarActivityContext; this.taskbarDragController = taskbarDragController; this.navButtonController = navButtonController; @@ -81,6 +123,16 @@ public class TaskbarControllers { this.taskbarEduController = taskbarEduController; this.taskbarAutohideSuspendController = taskbarAutoHideSuspendController; this.taskbarPopupController = taskbarPopupController; + this.taskbarForceVisibleImmersiveController = taskbarForceVisibleImmersiveController; + this.taskbarOverlayController = taskbarOverlayController; + this.taskbarAllAppsController = taskbarAllAppsController; + this.taskbarInsetsController = taskbarInsetsController; + this.voiceInteractionWindowController = voiceInteractionWindowController; + this.taskbarTranslationController = taskbarTranslationController; + this.taskbarSpringOnStashController = taskbarSpringOnStashController; + this.taskbarRecentAppsController = taskbarRecentAppsController; + this.taskbarEduTooltipController = taskbarEduTooltipController; + this.keyboardQuickSwitchController = keyboardQuickSwitchController; } /** @@ -88,8 +140,9 @@ public class TaskbarControllers { * TaskbarControllers instance, but should be careful to only access things that were created * in constructors for now, as some controllers may still be waiting for init(). */ - public void init(TaskbarSharedState sharedState) { + public void init(@NonNull TaskbarSharedState sharedState) { mAreAllControllersInitialized = false; + mSharedState = sharedState; taskbarDragController.init(this); navbarButtonsViewController.init(this); @@ -99,9 +152,36 @@ public class TaskbarControllers { taskbarScrimViewController.init(this); taskbarUnfoldAnimationController.init(this); taskbarKeyguardController.init(navbarButtonsViewController); + taskbarSpringOnStashController.init(this); stashedHandleViewController.init(this); - taskbarStashController.init(this, sharedState); + taskbarStashController.init(this, sharedState.setupUIVisible, mSharedState); taskbarEduController.init(this); + taskbarPopupController.init(this); + taskbarForceVisibleImmersiveController.init(this); + taskbarOverlayController.init(this); + taskbarAllAppsController.init(this, sharedState.allAppsVisible); + navButtonController.init(this); + taskbarInsetsController.init(this); + voiceInteractionWindowController.init(this); + taskbarRecentAppsController.init(this); + taskbarTranslationController.init(this); + taskbarEduTooltipController.init(this); + keyboardQuickSwitchController.init(this); + + mControllersToLog = new LoggableTaskbarController[] { + taskbarDragController, navButtonController, navbarButtonsViewController, + taskbarDragLayerController, taskbarScrimViewController, taskbarViewController, + taskbarUnfoldAnimationController, taskbarKeyguardController, + stashedHandleViewController, taskbarStashController, taskbarEduController, + taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController, + voiceInteractionWindowController, taskbarTranslationController, + taskbarEduTooltipController, keyboardQuickSwitchController + }; + mBackgroundRendererControllers = new BackgroundRendererController[] { + taskbarDragLayerController, taskbarScrimViewController, + voiceInteractionWindowController + }; + mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS); mAreAllControllersInitialized = true; for (Runnable postInitCallback : mPostInitCallbacks) { @@ -110,23 +190,45 @@ public class TaskbarControllers { mPostInitCallbacks.clear(); } + @Nullable + public TaskbarSharedState getSharedState() { + // This should only be null if called before init() and after destroy(). + return mSharedState; + } + public void onConfigurationChanged(@Config int configChanges) { navbarButtonsViewController.onConfigurationChanged(configChanges); + taskbarDragLayerController.onConfigurationChanged(); + keyboardQuickSwitchController.onConfigurationChanged(configChanges); } /** * Cleans up all controllers. */ public void onDestroy() { + mAreAllControllersInitialized = false; + mSharedState = null; + navbarButtonsViewController.onDestroy(); uiController.onDestroy(); rotationButtonController.onDestroy(); taskbarDragLayerController.onDestroy(); - taskbarKeyguardController.onDestroy(); taskbarUnfoldAnimationController.onDestroy(); taskbarViewController.onDestroy(); stashedHandleViewController.onDestroy(); taskbarAutohideSuspendController.onDestroy(); + taskbarPopupController.onDestroy(); + taskbarForceVisibleImmersiveController.onDestroy(); + taskbarOverlayController.onDestroy(); + navButtonController.onDestroy(); + taskbarInsetsController.onDestroy(); + voiceInteractionWindowController.onDestroy(); + taskbarRecentAppsController.onDestroy(); + keyboardQuickSwitchController.onDestroy(); + taskbarStashController.onDestroy(); + + mControllersToLog = null; + mBackgroundRendererControllers = null; } /** @@ -141,4 +243,57 @@ public class TaskbarControllers { mPostInitCallbacks.add(callback); } } + + protected void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarControllers:"); + + if (mControllersToLog == null) { + pw.println(String.format( + "%s\t%s", prefix, "All taskbar controllers have already been destroyed.")); + return; + } + + pw.println(String.format( + "%s\tmAreAllControllersInitialized=%b", prefix, mAreAllControllersInitialized)); + for (LoggableTaskbarController controller : mControllersToLog) { + controller.dumpLogs(prefix + "\t", pw); + } + uiController.dumpLogs(prefix + "\t", pw); + rotationButtonController.dumpLogs(prefix + "\t", pw); + } + + /** + * Returns a float property that animates roundness of the round corner above Taskbar. + */ + public AnimatedFloat getTaskbarCornerRoundness() { + return mCornerRoundness; + } + + private void updateCornerRoundness() { + if (mBackgroundRendererControllers == null) { + return; + } + + for (BackgroundRendererController controller : mBackgroundRendererControllers) { + controller.setCornerRoundness(mCornerRoundness.value); + } + } + + @VisibleForTesting + TaskbarActivityContext getTaskbarActivityContext() { + // Used to mock + return taskbarActivityContext; + } + + protected interface LoggableTaskbarController { + void dumpLogs(String prefix, PrintWriter pw); + } + + protected interface BackgroundRendererController { + /** + * Sets the roundness of the round corner above Taskbar. + * @param cornerRoundness 0 has no round corner, 1 has complete round corner. + */ + void setCornerRoundness(float cornerRoundness); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index 21d7411a56..4e3c4df863 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -15,6 +15,16 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; @@ -23,16 +33,20 @@ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.UserHandle; +import android.util.Pair; import android.view.DragEvent; import android.view.MotionEvent; +import android.view.SurfaceControl; import android.view.View; +import android.view.ViewRootImpl; +import android.window.SurfaceSyncer; import androidx.annotation.Nullable; import com.android.internal.logging.InstanceId; -import com.android.internal.logging.InstanceIdSequence; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DragSource; @@ -40,6 +54,7 @@ import com.android.launcher3.DropTarget; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.accessibility.DragViewStateAnnouncer; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragDriver; @@ -51,14 +66,31 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; 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.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.quickstep.util.LogUtils; +import com.android.quickstep.util.MultiValueUpdateListener; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.ClipDescriptionCompat; -import com.android.systemui.shared.system.LauncherAppsCompat; +import com.android.wm.shell.draganddrop.DragAndDropConstants; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Predicate; /** * Handles long click on Taskbar items to start a system drag and drop operation. */ -public class TaskbarDragController extends DragController { +public class TaskbarDragController extends DragController implements + TaskbarControllers.LoggableTaskbarController { + + private static final boolean DEBUG_DRAG_SHADOW_SURFACE = false; + private static final int ANIM_DURATION_RETURN_ICON_TO_TASKBAR = 300; private final int mDragIconSize; private final int[] mTempXY = new int[2]; @@ -72,7 +104,12 @@ public class TaskbarDragController extends DragController { - startInternalDrag(btv); - btv.getIcon().setIsDisabled(true); + DragView dragView = startInternalDrag(btv, dragPreviewProvider); + if (iconShift != null) { + dragView.animateShift(-iconShift.x, -iconShift.y); + } + btv.setIconDisabled(true); mControllers.taskbarAutohideSuspendController.updateFlag( TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, true); }); return true; } - private void startInternalDrag(BubbleTextView btv) { + private DragView startInternalDrag( + BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) { float iconScale = btv.getIcon().getAnimatedScale(); // Clear the pressed state if necessary @@ -112,7 +175,8 @@ public class TaskbarDragController extends DragController popupContainer = + PopupContainerWithArrow popupContainer = mControllers.taskbarPopupController.showForIcon(btv); if (popupContainer != null) { dragOptions.preDragCondition = popupContainer.createPreDragCondition(false); } } + if (dragOptions.preDragCondition == null) { + dragOptions.preDragCondition = new DragOptions.PreDragCondition() { + private DragView mDragView; - startDrag( + @Override + public boolean shouldStartDrag(double distanceDragged) { + return mDragView != null && mDragView.isAnimationFinished(); + } + + @Override + public void onPreDragStart(DropTarget.DragObject dragObject) { + mDragView = dragObject.dragView; + + if (FeatureFlags.ENABLE_TASKBAR_POPUP_MENU.get() + && !shouldStartDrag(0)) { + mDragView.setOnAnimationEndCallback(() -> { + // Drag might be cancelled during the DragView animation, so check + // mIsPreDrag again. + if (mIsInPreDrag) { + callOnDragStart(); + } + }); + } + } + + @Override + public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { + mDragView = null; + } + }; + } + + return startDrag( drawable, /* view = */ null, /* originalView = */ btv, dragLayerX, dragLayerY, (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */, - (WorkspaceItemInfo) btv.getTag(), + (ItemInfo) btv.getTag(), /* dragVisualizeOffset = */ null, dragRect, scale * iconScale, @@ -232,16 +310,23 @@ public class TaskbarDragController extends DragController instanceIds = + LogUtils.getShellShareableInstanceId(); // Need to share the same InstanceId between launcher3 and WM Shell (internal). - InstanceId internalInstanceId = new InstanceIdSequence( - com.android.launcher3.logging.InstanceId.INSTANCE_ID_MAX).newInstanceId(); - com.android.launcher3.logging.InstanceId launcherInstanceId = - new com.android.launcher3.logging.InstanceId(internalInstanceId.getId()); + InstanceId internalInstanceId = instanceIds.first; + com.android.launcher3.logging.InstanceId launcherInstanceId = instanceIds.second; intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId); + if (DisplayController.isTransientTaskbar(mActivity)) { + // Tell WM Shell to ignore drag events in the provided transient taskbar region. + TaskbarDragLayer dragLayer = mControllers.taskbarActivityContext.getDragLayer(); + int[] locationOnScreen = dragLayer.getLocationOnScreen(); + RectF disallowExternalDropRegion = new RectF(dragLayer.getLastDrawnTransientRect()); + disallowExternalDropRegion.offset(locationOnScreen[0], locationOnScreen[1]); + intent.putExtra(DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION, + disallowExternalDropRegion); + } ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent)); if (btv.startDragAndDrop(clipData, shadowBuilder, null /* localState */, - View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE)) { - onSystemDragStarted(); + View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE + | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION)) { + onSystemDragStarted(btv); mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo) .withInstanceId(launcherInstanceId) @@ -313,7 +418,7 @@ public class TaskbarDragController extends DragController { switch (dragEvent.getAction()) { @@ -322,7 +427,14 @@ public class TaskbarDragController extends DragController { + dragView.setTranslationX(x); + dragView.setTranslationY(y); + dragView.setScaleX(scale); + dragView.setScaleY(scale); + dragView.setAlpha(alpha); + }); + mReturnAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callOnDragEnd(); + dragView.remove(); + dragView.clearAnimation(); + // Do this after callOnDragEnd(), because we use mReturnAnimator != null to + // imply the drag was canceled rather than successful. + mReturnAnimator = null; + } + }); + mReturnAnimator.start(); + } + super.endDrag(); + } + @Override protected void callOnDragEnd() { super.callOnDragEnd(); maybeOnDragEnd(); } + private void animateGlobalDragViewToOriginalPosition(BubbleTextView btv, + DragEvent dragEvent) { + SurfaceControl dragSurface = dragEvent.getDragSurface(); + + // For top level icons, the target is the icon itself + View target = findTaskbarTargetForIconView(btv); + + float fromX = dragEvent.getX() - dragEvent.getOffsetX(); + float fromY = dragEvent.getY() - dragEvent.getOffsetY(); + final ViewRootImpl viewRoot = target.getViewRootImpl(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + setupReturnDragAnimator(fromX, fromY, btv, + (x, y, scale, alpha) -> { + tx.setPosition(dragSurface, x, y); + tx.setScale(dragSurface, scale, scale); + tx.setAlpha(dragSurface, alpha); + tx.apply(); + }); + + mReturnAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + + @Override + public void onAnimationCancel(Animator animation) { + cleanUpSurface(); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) { + return; + } + cleanUpSurface(); + } + + private void cleanUpSurface() { + tx.close(); + maybeOnDragEnd(); + // Synchronize removing the drag surface with the next draw after calling + // maybeOnDragEnd() + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + transaction.remove(dragSurface); + SurfaceSyncer syncer = new SurfaceSyncer(); + int syncId = syncer.setupSync(transaction::close); + syncer.addToSync(syncId, viewRoot.getView()); + syncer.addTransactionToSync(syncId, transaction); + syncer.markSyncReady(syncId); + // Do this after maybeOnDragEnd(), because we use mReturnAnimator != null to imply + // the drag was canceled rather than successful. + mReturnAnimator = null; + } + }); + mReturnAnimator.start(); + } + + private View findTaskbarTargetForIconView(@NonNull View iconView) { + Object tag = iconView.getTag(); + TaskbarViewController taskbarViewController = mControllers.taskbarViewController; + + if (tag instanceof ItemInfo) { + ItemInfo item = (ItemInfo) tag; + if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) { + if (mDisallowGlobalDrag) { + // We're dragging in taskbarAllApps, we don't have folders or shortcuts + return iconView; + } + // Since all apps closes when the drag starts, target the all apps button instead. + return taskbarViewController.getAllAppsButtonView(); + } else if (item.container >= 0) { + // Since folders close when the drag starts, target the folder icon instead. + Predicate matcher = ItemInfoMatcher.forFolderMatch( + ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id))); + return taskbarViewController.getFirstIconMatch(matcher); + } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) { + // Find first icon with same package/user as the deep shortcut. + Predicate packageUserMatcher = ItemInfoMatcher.ofPackages( + Collections.singleton(item.getTargetPackage()), item.user); + return taskbarViewController.getFirstIconMatch(packageUserMatcher); + } + } + return iconView; + } + + private void setupReturnDragAnimator(float fromX, float fromY, View originalView, + TaskbarReturnPropertiesListener animListener) { + // Finish any pending return animation before starting a new return + if (mReturnAnimator != null) { + mReturnAnimator.end(); + } + + // For top level icons, the target is the icon itself + View target = findTaskbarTargetForIconView(originalView); + + int[] toPosition = target.getLocationOnScreen(); + float iconSize = target.getWidth(); + if (target instanceof BubbleTextView) { + Rect bounds = new Rect(); + ((BubbleTextView) target).getSourceVisualDragBounds(bounds); + toPosition[0] += bounds.left; + toPosition[1] += bounds.top; + iconSize = bounds.width(); + } + float toScale = iconSize / mDragIconSize; + float toAlpha = (target == originalView) ? 1f : 0f; + MultiValueUpdateListener listener = new MultiValueUpdateListener() { + final FloatProp mDx = new FloatProp(fromX, toPosition[0], 0, + ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.FAST_OUT_SLOW_IN); + final FloatProp mDy = new FloatProp(fromY, toPosition[1], 0, + ANIM_DURATION_RETURN_ICON_TO_TASKBAR, + FAST_OUT_SLOW_IN); + final FloatProp mScale = new FloatProp(1f, toScale, 0, + ANIM_DURATION_RETURN_ICON_TO_TASKBAR, FAST_OUT_SLOW_IN); + final FloatProp mAlpha = new FloatProp(1f, toAlpha, 0, + ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCEL_2); + @Override + public void onUpdate(float percent, boolean initOnly) { + animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value); + } + }; + mReturnAnimator = ValueAnimator.ofFloat(0f, 1f); + mReturnAnimator.setDuration(ANIM_DURATION_RETURN_ICON_TO_TASKBAR); + mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + mReturnAnimator.addUpdateListener(listener); + } + @Override protected float getX(MotionEvent ev) { // We will resize to fill the screen while dragging, so use screen coordinates. This ensures @@ -371,7 +661,7 @@ public class TaskbarDragController extends DragController iconsToAnimate = mControllers.uiController.getAppIconsForEdu() - .filter(appInfo -> !TextUtils.equals(appInfo.title, itemInfo.title)) - .map(appInfo -> appInfo.bitmap) - .filter(bitmap -> !bitmap.isNullOrLowRes()) - .collect(Collectors.toList()); - // Pick n icons at random. - Collections.shuffle(iconsToAnimate); - if (iconsToAnimate.size() > WAVE_ANIM_SLOT_MACHINE_NUM_ICONS) { - iconsToAnimate = iconsToAnimate.subList(0, WAVE_ANIM_SLOT_MACHINE_NUM_ICONS); - } - Animator slotMachineAnim = predictedAppIcon.createSlotMachineAnim(iconsToAnimate); - if (slotMachineAnim != null) { - iconAnim.play(slotMachineAnim.setDuration(WAVE_ANIM_SLOT_MACHINE_DURATION)); - } - } - - iconAnim.setStartDelay(WAVE_ANIM_STAGGER * i); - waveAnim.play(iconAnim); - } - waveAnim.setStartDelay(WAVE_ANIM_DELAY); - return waveAnim; + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarEduController:"); + pw.println(prefix + "\tisShowingEdu=" + (mTaskbarEduView != null)); } /** * Callbacks for {@link TaskbarEduView} to interact with its controller. */ class TaskbarEduCallbacks { - void onPageChanged(int currentPage, int pageCount) { + void onPageChanged(int prevPage, int currentPage, int pageCount) { + // Reset previous pages' animation. + LottieAnimationView prevAnimation = mPagedView.getChildAt(prevPage) + .findViewById(R.id.animation); + prevAnimation.cancelAnimation(); + prevAnimation.setFrame(0); + + mPagedView.getChildAt(currentPage) + .findViewById(R.id.animation) + .playAnimation(); + if (currentPage == 0) { mTaskbarEduView.updateStartButton(R.string.taskbar_edu_close, v -> mTaskbarEduView.close(true /* animate */)); @@ -206,5 +115,17 @@ public class TaskbarEduController { v -> mTaskbarEduView.snapToPage(currentPage + 1)); } } + + int getIconLayoutBoundsWidth() { + return mControllers.taskbarViewController.getIconLayoutWidth(); + } + + int getOpenDuration() { + return mControllers.taskbarOverlayController.getOpenDuration(); + } + + int getCloseDuration() { + return mControllers.taskbarOverlayController.getCloseDuration(); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduPagedView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduPagedView.java index 5efcc4df4c..6cd6512391 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduPagedView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduPagedView.java @@ -48,11 +48,11 @@ public class TaskbarEduPagedView extends PagedView { void setControllerCallbacks(TaskbarEduCallbacks controllerCallbacks) { mControllerCallbacks = controllerCallbacks; - mControllerCallbacks.onPageChanged(getCurrentPage(), getPageCount()); + mControllerCallbacks.onPageChanged(getCurrentPage(), getCurrentPage(), getPageCount()); } @Override - protected int getChildGap() { + protected int getChildGap(int fromIndex, int toIndex) { return mTaskbarEduView.getPaddingLeft() + mTaskbarEduView.getPaddingRight(); } @@ -67,7 +67,7 @@ public class TaskbarEduPagedView extends PagedView { @Override protected void notifyPageSwitchListener(int prevPage) { super.notifyPageSwitchListener(prevPage); - mControllerCallbacks.onPageChanged(getCurrentPage(), getPageCount()); + mControllerCallbacks.onPageChanged(prevPage, getCurrentPage(), getPageCount()); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt new file mode 100644 index 0000000000..bab014d199 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.provider.Settings +import android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.View +import android.view.ViewGroup +import android.view.animation.Interpolator +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.anim.AnimatorListeners +import com.android.launcher3.popup.RoundedArrowDrawable +import com.android.launcher3.util.Themes +import com.android.launcher3.views.ActivityContext +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.systemui.animation.Interpolators.STANDARD + +private const val ENTER_DURATION_MS = 300L +private const val EXIT_DURATION_MS = 150L + +/** Floating tooltip for Taskbar education. */ +class TaskbarEduTooltip +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : AbstractFloatingView(context, attrs, defStyleAttr) { + + private val activityContext: ActivityContext = ActivityContext.lookupContext(context) + + private val backgroundColor = + Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface) + + private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) + private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) + private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) + private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) + + private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta) + private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta) + + /** Container where the tooltip's body should be inflated. */ + lateinit var content: ViewGroup + private set + private lateinit var arrow: View + + /** Callback invoked when the tooltip is being closed. */ + var onCloseCallback: () -> Unit = {} + private var openCloseAnimator: AnimatorSet? = null + + /** Animates the tooltip into view. */ + fun show() { + if (isOpen) { + return + } + mIsOpen = true + activityContext.dragLayer.addView(this) + openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() } + } + + override fun onFinishInflate() { + super.onFinishInflate() + + content = requireViewById(R.id.content) + arrow = requireViewById(R.id.arrow) + arrow.background = + RoundedArrowDrawable( + arrowWidth, + arrowHeight, + arrowPointRadius, + tooltipCornerRadius, + measuredWidth.toFloat(), + measuredHeight.toFloat(), + (measuredWidth - arrowWidth) / 2, // arrowOffsetX + 0f, // arrowOffsetY + false, // isPointingUp + true, // leftAligned + backgroundColor, + ) + } + + override fun handleClose(animate: Boolean) { + if (!isOpen) { + return + } + + onCloseCallback() + if (!animate) { + return closeComplete() + } + + openCloseAnimator?.cancel() + openCloseAnimator = createOpenCloseAnimator(isOpening = false) + openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete)) + openCloseAnimator?.start() + } + + override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0 + + override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) { + close(true) + } + return false + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) + } + + private fun closeComplete() { + openCloseAnimator?.cancel() + openCloseAnimator = null + mIsOpen = false + activityContext.dragLayer.removeView(this) + } + + private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet { + val duration: Long + val alphaValues: FloatArray + val translateYValues: FloatArray + val fadeInterpolator: Interpolator + val translateYInterpolator: Interpolator + + if (isOpening) { + duration = ENTER_DURATION_MS + alphaValues = floatArrayOf(0f, 1f) + translateYValues = floatArrayOf(enterYDelta, 0f) + fadeInterpolator = STANDARD + translateYInterpolator = EMPHASIZED_DECELERATE + } else { + duration = EXIT_DURATION_MS + alphaValues = floatArrayOf(1f, 0f) + translateYValues = floatArrayOf(0f, exitYDelta) + fadeInterpolator = EMPHASIZED_ACCELERATE + translateYInterpolator = EMPHASIZED_ACCELERATE + } + + val fade = + ValueAnimator.ofFloat(*alphaValues).apply { + interpolator = fadeInterpolator + addUpdateListener { + val alpha = it.animatedValue as Float + content.alpha = alpha + arrow.alpha = alpha + } + } + + val translateY = + ValueAnimator.ofFloat(*translateYValues).apply { + interpolator = translateYInterpolator + addUpdateListener { + val translationY = it.animatedValue as Float + content.translationY = translationY + arrow.translationY = translationY + } + } + + return AnimatorSet().apply { + this.duration = duration + playTogether(fade, translateY) + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt new file mode 100644 index 0000000000..277e444e7a --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import android.graphics.PorterDuff.Mode.SRC_ATOP +import android.graphics.PorterDuffColorFilter +import android.os.Bundle +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import androidx.annotation.IntDef +import androidx.annotation.LayoutRes +import com.airbnb.lottie.LottieAnimationView +import com.airbnb.lottie.LottieProperty.COLOR_FILTER +import com.airbnb.lottie.model.KeyPath +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_EDU_TOOLTIP +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN +import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP +import java.io.PrintWriter + +/** First EDU step for swiping up to show transient Taskbar. */ +const val TOOLTIP_STEP_SWIPE = 0 +/** Second EDU step for explaining Taskbar functionality when unstashed. */ +const val TOOLTIP_STEP_FEATURES = 1 +/** + * EDU is completed. + * + * This value should match the maximum count for [TASKBAR_EDU_TOOLTIP_STEP]. + */ +const val TOOLTIP_STEP_NONE = 2 + +/** Current step in the tooltip EDU flow. */ +@Retention(AnnotationRetention.SOURCE) +@IntDef(TOOLTIP_STEP_SWIPE, TOOLTIP_STEP_FEATURES, TOOLTIP_STEP_NONE) +annotation class TaskbarEduTooltipStep + +/** Controls stepping through the Taskbar tooltip EDU. */ +class TaskbarEduTooltipController(val activityContext: TaskbarActivityContext) : + LoggableTaskbarController { + + private val isTooltipEnabled: Boolean + get() = !Utilities.isRunningInTestHarness() && ENABLE_TASKBAR_EDU_TOOLTIP.get() + private val isOpen: Boolean + get() = tooltip?.isOpen ?: false + val isBeforeTooltipFeaturesStep: Boolean + get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES + private lateinit var controllers: TaskbarControllers + + @TaskbarEduTooltipStep + var tooltipStep: Int + get() { + return activityContext.onboardingPrefs?.getCount(TASKBAR_EDU_TOOLTIP_STEP) + ?: TOOLTIP_STEP_NONE + } + private set(step) { + activityContext.onboardingPrefs?.setEventCount(step, TASKBAR_EDU_TOOLTIP_STEP) + } + + private var tooltip: TaskbarEduTooltip? = null + + fun init(controllers: TaskbarControllers) { + this.controllers = controllers + } + + /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */ + fun maybeShowSwipeEdu() { + if ( + !isTooltipEnabled || + !DisplayController.isTransientTaskbar(activityContext) || + tooltipStep > TOOLTIP_STEP_SWIPE + ) { + return + } + + tooltipStep = TOOLTIP_STEP_FEATURES + inflateTooltip(R.layout.taskbar_edu_swipe) + tooltip?.run { + requireViewById(R.id.swipe_animation).supportLightTheme() + show() + } + } + + /** + * Shows feature EDU tooltip if this step has not been seen. + * + * If [TOOLTIP_STEP_SWIPE] has not been seen at this point, the first step is skipped because a + * swipe up is necessary to show this step. + */ + fun maybeShowFeaturesEdu() { + if (!isTooltipEnabled || tooltipStep > TOOLTIP_STEP_FEATURES) { + return + } + + tooltipStep = TOOLTIP_STEP_NONE + inflateTooltip(R.layout.taskbar_edu_features) + tooltip?.run { + val splitscreenAnim = requireViewById(R.id.splitscreen_animation) + val suggestionsAnim = requireViewById(R.id.suggestions_animation) + val settingsAnim = requireViewById(R.id.settings_animation) + val settingsEdu = requireViewById(R.id.settings_edu) + splitscreenAnim.supportLightTheme() + suggestionsAnim.supportLightTheme() + settingsAnim.supportLightTheme() + if (DisplayController.isTransientTaskbar(activityContext)) { + splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient) + suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient) + settingsEdu.visibility = GONE + } else { + splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_persistent) + suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_persistent) + settingsEdu.visibility = VISIBLE + } + + findViewById(R.id.done_button)?.setOnClickListener { hide() } + if (DisplayController.isTransientTaskbar(activityContext)) { + (layoutParams as ViewGroup.MarginLayoutParams).bottomMargin += + activityContext.deviceProfile.taskbarHeight + } + show() + } + } + + /** Closes the current [tooltip]. */ + fun hide() = tooltip?.close(true) + + /** Initializes [tooltip] with content from [contentResId]. */ + private fun inflateTooltip(@LayoutRes contentResId: Int) { + val overlayContext = controllers.taskbarOverlayController.requestWindow() + val tooltip = + overlayContext.layoutInflater.inflate( + R.layout.taskbar_edu_tooltip, + overlayContext.dragLayer, + false + ) as TaskbarEduTooltip + + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, + true + ) + + tooltip.onCloseCallback = { + this.tooltip = null + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, + false + ) + controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) + } + tooltip.accessibilityDelegate = createAccessibilityDelegate() + + overlayContext.layoutInflater.inflate(contentResId, tooltip.content, true) + this.tooltip = tooltip + } + + private fun createAccessibilityDelegate() = + object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == R.id.close) { + hide() + return true + } + return super.performAccessibilityAction(host, action, args) + } + + override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) { + super.onPopulateAccessibilityEvent(host, event) + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + event.text.add(host.context?.getText(R.string.taskbar_edu_a11y_title)) + } + } + + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityNodeInfo.AccessibilityAction( + R.id.close, + host.context?.getText(R.string.taskbar_edu_close) + ) + ) + } + } + + override fun dumpLogs(prefix: String?, pw: PrintWriter?) { + pw?.println(prefix + "TaskbarEduTooltipController:") + pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled") + pw?.println("$prefix\tisOpen=$isOpen") + pw?.println("$prefix\ttooltipStep=$tooltipStep") + } +} + +/** + * Maps colors in the dark-themed Lottie assets to their light-themed equivalents. + * + * For instance, `".blue100" to R.color.lottie_blue400` means objects that are material blue100 in + * dark theme should be changed to material blue400 in light theme. + */ +private val DARK_TO_LIGHT_COLORS = + mapOf( + ".blue100" to R.color.lottie_blue400, + ".blue400" to R.color.lottie_blue600, + ".green100" to R.color.lottie_green400, + ".green400" to R.color.lottie_green600, + ".grey300" to R.color.lottie_grey600, + ".grey400" to R.color.lottie_grey700, + ".grey800" to R.color.lottie_grey200, + ".red400" to R.color.lottie_red600, + ".yellow100" to R.color.lottie_yellow400, + ".yellow400" to R.color.lottie_yellow600, + ) + +private fun LottieAnimationView.supportLightTheme() { + if (Utilities.isDarkTheme(context)) { + return + } + + addLottieOnCompositionLoadedListener { + DARK_TO_LIGHT_COLORS.forEach { (key, color) -> + addValueCallback(KeyPath("**", key, "**"), COLOR_FILTER) { + PorterDuffColorFilter(context.getColor(color), SRC_ATOP) + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java index 8525427a64..5702b6bb5c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java @@ -15,29 +15,32 @@ */ package com.android.launcher3.taskbar; -import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; import android.animation.PropertyValuesHolder; import android.content.Context; import android.graphics.Rect; +import android.provider.Settings; import android.util.AttributeSet; import android.util.Pair; import android.view.View; +import android.view.animation.Interpolator; import android.widget.Button; import com.android.launcher3.Insettable; import com.android.launcher3.R; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import com.android.launcher3.views.AbstractSlideInView; /** Education view about the Taskbar. */ -public class TaskbarEduView extends AbstractSlideInView +public class TaskbarEduView extends AbstractSlideInView implements Insettable { - private static final int DEFAULT_OPEN_DURATION = 500; - private static final int DEFAULT_CLOSE_DURATION = 200; - private final Rect mInsets = new Rect(); + // Initialized in init. + private TaskbarEduController.TaskbarEduCallbacks mTaskbarEduCallbacks; + private Button mStartButton; private Button mEndButton; private TaskbarEduPagedView mPagedView; @@ -55,11 +58,17 @@ public class TaskbarEduView extends AbstractSlideInView if (mPagedView != null) { mPagedView.setControllerCallbacks(callbacks); } + mTaskbarEduCallbacks = callbacks; } @Override protected void handleClose(boolean animate) { - handleClose(animate, DEFAULT_CLOSE_DURATION); + handleClose(animate, mTaskbarEduCallbacks.getCloseDuration()); + } + + @Override + protected Interpolator getIdleInterpolator() { + return EMPHASIZED; } @Override @@ -92,6 +101,30 @@ public class TaskbarEduView extends AbstractSlideInView getPopupContainer().addView(this, 1); } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int contentWidth = Math.min(getContentAreaWidth(), getMeasuredWidth()); + contentWidth = Math.max(contentWidth, mTaskbarEduCallbacks.getIconLayoutBoundsWidth()); + int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); + + mContent.measure(contentAreaWidthSpec, MeasureSpec.UNSPECIFIED); + } + + private int getContentAreaWidth() { + return mTaskbarEduCallbacks.getIconLayoutBoundsWidth() + + getResources().getDimensionPixelSize(R.dimen.taskbar_edu_horizontal_margin) * 2; + } + /** Show the Education flow. */ public void show() { attachToContainer(); @@ -130,8 +163,8 @@ public class TaskbarEduView extends AbstractSlideInView mIsOpen = true; mOpenCloseAnimator.setValues( PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); - mOpenCloseAnimator.setInterpolator(AGGRESSIVE_EASE); - mOpenCloseAnimator.setDuration(DEFAULT_OPEN_DURATION).start(); + mOpenCloseAnimator.setInterpolator(EMPHASIZED); + mOpenCloseAnimator.setDuration(mTaskbarEduCallbacks.getOpenDuration()).start(); } void snapToPage(int page) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java new file mode 100644 index 0000000000..ffaee455d9 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java @@ -0,0 +1,181 @@ +/* + * 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; + +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; + +import static com.android.launcher3.taskbar.NavbarButtonsViewController.ALPHA_INDEX_IMMERSIVE_MODE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IMMERSIVE_MODE; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.MotionEvent; +import android.view.View; + +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.util.MultiPropertyFactory; +import com.android.launcher3.util.TouchController; + +/** + * Controller for taskbar when force visible in immersive mode is set. + */ +public class TaskbarForceVisibleImmersiveController implements TouchController { + private static final int NAV_BAR_ICONS_DIM_ANIMATION_START_DELAY_MS = 4500; + private static final int NAV_BAR_ICONS_DIM_ANIMATION_DURATION_MS = 500; + private static final int NAV_BAR_ICONS_UNDIM_ANIMATION_DURATION_MS = 250; + private static final float NAV_BAR_ICONS_DIM_PCT = 0.15f; + private static final float NAV_BAR_ICONS_UNDIM_PCT = 1f; + + private final TaskbarActivityContext mContext; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Runnable mDimmingRunnable = this::dimIcons; + private final Runnable mUndimmingRunnable = this::undimIcons; + private final AnimatedFloat mIconAlphaForDimming = new AnimatedFloat( + this::updateIconDimmingAlpha); + private final View.AccessibilityDelegate mKidsModeAccessibilityDelegate = + new View.AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_ACCESSIBILITY_FOCUS || action == ACTION_CLICK) { + // Animate undimming of icons on an a11y event, followed by starting the + // dimming animation (after its timeout has expired). Both can be called in + // succession, as the playing of the two animations in a row is managed by + // mHandler's message queue. + startIconUndimming(); + startIconDimming(); + } + return super.performAccessibilityAction(host, action, args); + } + }; + + // Initialized in init. + private TaskbarControllers mControllers; + private boolean mIsImmersiveMode; + + public TaskbarForceVisibleImmersiveController(TaskbarActivityContext context) { + mContext = context; + } + + /** + * Initialize controllers. + */ + public void init(TaskbarControllers controllers) { + mControllers = controllers; + } + + /** Update values tracked via sysui flags. */ + public void updateSysuiFlags(int sysuiFlags) { + mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_IMMERSIVE_MODE) != 0; + if (mContext.isNavBarForceVisible()) { + if (mIsImmersiveMode) { + startIconDimming(); + } else { + startIconUndimming(); + } + mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate( + mKidsModeAccessibilityDelegate); + mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate( + mKidsModeAccessibilityDelegate); + } else { + mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null); + mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null); + } + } + + /** Clean up animations. */ + public void onDestroy() { + startIconUndimming(); + mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null); + mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null); + } + + private void startIconUndimming() { + mHandler.removeCallbacks(mDimmingRunnable); + mHandler.removeCallbacks(mUndimmingRunnable); + mHandler.post(mUndimmingRunnable); + } + + private void undimIcons() { + mIconAlphaForDimming.animateToValue(NAV_BAR_ICONS_UNDIM_PCT).setDuration( + NAV_BAR_ICONS_UNDIM_ANIMATION_DURATION_MS).start(); + } + + private void startIconDimming() { + mHandler.removeCallbacks(mDimmingRunnable); + int accessibilityDimmingTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis( + mContext, NAV_BAR_ICONS_DIM_ANIMATION_START_DELAY_MS, + (FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS)); + mHandler.postDelayed(mDimmingRunnable, accessibilityDimmingTimeout); + } + + private void dimIcons() { + mIconAlphaForDimming.animateToValue(NAV_BAR_ICONS_DIM_PCT).setDuration( + NAV_BAR_ICONS_DIM_ANIMATION_DURATION_MS).start(); + } + + /** + * Returns whether the taskbar is always visible in immersive mode. + */ + private boolean isNavbarShownInImmersiveMode() { + return mIsImmersiveMode && mContext.isNavBarForceVisible(); + } + + private void updateIconDimmingAlpha() { + if (mControllers == null || mControllers.navbarButtonsViewController == null) { + return; + } + + MultiPropertyFactory ba = + mControllers.navbarButtonsViewController.getBackButtonAlpha(); + if (ba != null) { + ba.get(ALPHA_INDEX_IMMERSIVE_MODE).setValue(mIconAlphaForDimming.value); + } + MultiPropertyFactory ha = + mControllers.navbarButtonsViewController.getHomeButtonAlpha(); + if (ba != null) { + ha.get(ALPHA_INDEX_IMMERSIVE_MODE).setValue(mIconAlphaForDimming.value); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (!isNavbarShownInImmersiveMode() + || mControllers.taskbarStashController.supportsManualStashing()) { + return false; + } + return onControllerTouchEvent(ev); + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + startIconUndimming(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + startIconDimming(); + break; + } + return false; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt new file mode 100644 index 0000000000..c299cc58af --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -0,0 +1,290 @@ +/* + * 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 + +import android.graphics.Insets +import android.graphics.Region +import android.view.InsetsFrameProvider +import android.view.InsetsFrameProvider.SOURCE_DISPLAY +import android.view.InsetsFrameProvider.SOURCE_FRAME +import android.view.InsetsState +import android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES +import android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT +import android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR +import android.view.InsetsState.ITYPE_LEFT_GESTURES +import android.view.InsetsState.ITYPE_RIGHT_GESTURES +import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME +import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD +import android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION +import com.android.internal.policy.GestureNavigationSettingsObserver +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R +import com.android.launcher3.anim.AlphaUpdateListener +import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import java.io.PrintWriter + +/** Handles the insets that Taskbar provides to underlying apps and the IME. */ +class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController { + + /** The bottom insets taskbar provides to the IME when IME is visible. */ + val taskbarHeightForIme: Int = context.resources.getDimensionPixelSize(R.dimen.taskbar_ime_size) + private val touchableRegion: Region = Region() + private val deviceProfileChangeListener = { _: DeviceProfile -> + onTaskbarWindowHeightOrInsetsChanged() + } + private val gestureNavSettingsObserver = + GestureNavigationSettingsObserver( + context.mainThreadHandler, + context, + this::onTaskbarWindowHeightOrInsetsChanged + ) + + // Initialized in init. + private lateinit var controllers: TaskbarControllers + private lateinit var windowLayoutParams: WindowManager.LayoutParams + + fun init(controllers: TaskbarControllers) { + this.controllers = controllers + windowLayoutParams = context.windowLayoutParams + windowLayoutParams.insetsRoundedCornerFrame = true + onTaskbarWindowHeightOrInsetsChanged() + + context.addOnDeviceProfileChangeListener(deviceProfileChangeListener) +// gestureNavSettingsObserver.registerForCallingUser() + } + + fun onDestroy() { + context.removeOnDeviceProfileChangeListener(deviceProfileChangeListener) + gestureNavSettingsObserver.unregister() + } + + fun onTaskbarWindowHeightOrInsetsChanged() { + if (context.isGestureNav) { + setProvidesInsetsTypes( + windowLayoutParams, + intArrayOf( + ITYPE_EXTRA_NAVIGATION_BAR, + ITYPE_BOTTOM_TAPPABLE_ELEMENT, + ITYPE_BOTTOM_MANDATORY_GESTURES, + ITYPE_LEFT_GESTURES, + ITYPE_RIGHT_GESTURES, + ), + intArrayOf( + SOURCE_FRAME, + SOURCE_FRAME, + SOURCE_FRAME, + SOURCE_DISPLAY, + SOURCE_DISPLAY + ) + ) + } else { + setProvidesInsetsTypes( + windowLayoutParams, + intArrayOf( + ITYPE_EXTRA_NAVIGATION_BAR, + ITYPE_BOTTOM_TAPPABLE_ELEMENT, + ITYPE_BOTTOM_MANDATORY_GESTURES + ), + intArrayOf( + SOURCE_FRAME, + SOURCE_FRAME, + SOURCE_FRAME + ) + ) + } + + val touchableHeight = controllers.taskbarStashController.touchableHeight + touchableRegion.set( + 0, + windowLayoutParams.height - touchableHeight, + context.deviceProfile.widthPx, + windowLayoutParams.height + ) + val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps + val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps + val res = context.resources + +// for (provider in windowLayoutParams.providedInsets) { +// if ( +// provider.type == ITYPE_EXTRA_NAVIGATION_BAR || +// provider.type == ITYPE_BOTTOM_MANDATORY_GESTURES +// ) { +// provider.insetsSize = getInsetsByNavMode(contentHeight) +// } else if (provider.type == ITYPE_BOTTOM_TAPPABLE_ELEMENT) { +// provider.insetsSize = getInsetsByNavMode(tappableHeight) +// } else if (provider.type == ITYPE_LEFT_GESTURES) { +// provider.insetsSize = +// Insets.of( +// gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res), +// 0, +// 0, +// 0 +// ) +// } else if (provider.type == ITYPE_RIGHT_GESTURES) { +// provider.insetsSize = +// Insets.of( +// 0, +// 0, +// gestureNavSettingsObserver.getRightSensitivityForCallingUser(res), +// 0 +// ) +// } +// } + + val imeInsetsSize = getInsetsByNavMode(taskbarHeightForIme) + val insetsSizeOverride = + arrayOf( + InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize), + ) + // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled. + val visInsetsSizeForGestureNavTappableElement = getInsetsByNavMode(0) + val insetsSizeOverrideForGestureNavTappableElement = + arrayOf( + InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize), + InsetsFrameProvider.InsetsSizeOverride( + TYPE_VOICE_INTERACTION, + visInsetsSizeForGestureNavTappableElement + ), + ) +// for (provider in windowLayoutParams.providedInsets) { +// if (context.isGestureNav && provider.type == ITYPE_BOTTOM_TAPPABLE_ELEMENT) { +// provider.insetsSizeOverrides = insetsSizeOverrideForGestureNavTappableElement +// } else if (provider.type != ITYPE_LEFT_GESTURES +// && provider.type != ITYPE_RIGHT_GESTURES) { +// // We only override insets at the bottom of the screen +// provider.insetsSizeOverrides = insetsSizeOverride +// } +// } + context.notifyUpdateLayoutParams() + } + + /** + * @return [Insets] where the [bottomInset] is either used as a bottom inset or + * + * ``` + * right/left inset if using 3 button nav + * ``` + */ + private fun getInsetsByNavMode(bottomInset: Int): Insets { + val devicePortrait = !context.deviceProfile.isLandscape + if (!TaskbarManager.isPhoneButtonNavMode(context) || devicePortrait) { + // Taskbar or portrait phone mode + return Insets.of(0, 0, 0, bottomInset) + } + + // TODO(b/230394142): seascape + return Insets.of(0, 0, bottomInset, 0) + } + + /** + * Sets {@param providesInsetsTypes} as the inset types provided by {@param params}. + * + * @param params The window layout params. + * @param providesInsetsTypes The inset types we would like this layout params to provide. + */ + fun setProvidesInsetsTypes( + params: WindowManager.LayoutParams, + providesInsetsTypes: IntArray, + providesInsetsSources: IntArray + ) { +// params.providedInsets = arrayOfNulls(providesInsetsTypes.size) +// for (i in providesInsetsTypes.indices) { +// params.providedInsets[i] = +// InsetsFrameProvider(providesInsetsTypes[i], providesInsetsSources[i], null, null) +// } + } + + /** + * Called to update the touchable insets. + * + * @see InternalInsetsInfo.setTouchableInsets + */ + fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) { + insetsInfo.touchableRegion.setEmpty() + // Always have nav buttons be touchable + controllers.navbarButtonsViewController.addVisibleButtonsRegion( + context.dragLayer, + insetsInfo.touchableRegion + ) + var insetsIsTouchableRegion = true + if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) { + // Let touches pass through us. + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } else if ( + controllers.navbarButtonsViewController.isImeVisible && + controllers.taskbarStashController.isStashed() + ) { + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } else if (!controllers.uiController.isTaskbarTouchable) { + // Let touches pass through us. + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } else if (controllers.taskbarDragController.isSystemDragInProgress) { + // Let touches pass through us. + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } else if (AbstractFloatingView.hasOpenView(context, TYPE_TASKBAR_OVERLAY_PROXY)) { + // Let touches pass through us if icons are hidden. + if (controllers.taskbarViewController.areIconsVisible()) { + insetsInfo.touchableRegion.set(touchableRegion) + } + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } else if ( + controllers.taskbarViewController.areIconsVisible() || + AbstractFloatingView.hasOpenView(context, AbstractFloatingView.TYPE_ALL) || + context.isNavBarKidsModeActive + ) { + // Taskbar has some touchable elements, take over the full taskbar area + insetsInfo.setTouchableInsets( + if (context.isTaskbarWindowFullscreen) { + TOUCHABLE_INSETS_FRAME + } else { + insetsInfo.touchableRegion.set(touchableRegion) + TOUCHABLE_INSETS_REGION + } + ) + insetsIsTouchableRegion = false + } else { + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } + context.excludeFromMagnificationRegion(insetsIsTouchableRegion) + } + + override fun dumpLogs(prefix: String, pw: PrintWriter) { + pw.println(prefix + "TaskbarInsetsController:") + pw.println("$prefix\twindowHeight=${windowLayoutParams.height}") +// for (provider in windowLayoutParams.providedInsets) { +// pw.print( +// "$prefix\tprovidedInsets: (type=" + +// InsetsState.typeToString(provider.type) + +// " insetsSize=" + +// provider.insetsSize +// ) +// if (provider.insetsSizeOverrides != null) { +// pw.print(" insetsSizeOverrides={") +// for ((i, overrideSize) in provider.insetsSizeOverrides.withIndex()) { +// if (i > 0) pw.print(", ") +// pw.print(overrideSize) +// } +// pw.print("})") +// } +// pw.println() +// } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java index 5fc0695564..03d08eb4b3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java @@ -1,42 +1,48 @@ package com.android.launcher3.taskbar; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK; +import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP; import android.app.KeyguardManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; + +import com.android.launcher3.AbstractFloatingView; +import com.android.systemui.shared.system.QuickStepContract; + +import java.io.PrintWriter; /** * Controller for managing keyguard state for taskbar */ -public class TaskbarKeyguardController { +public class TaskbarKeyguardController implements TaskbarControllers.LoggableTaskbarController { - private static final int KEYGUARD_SYSUI_FLAGS = SYSUI_STATE_BOUNCER_SHOWING | - SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_DEVICE_DOZING | - SYSUI_STATE_OVERVIEW_DISABLED | SYSUI_STATE_HOME_DISABLED | - SYSUI_STATE_BACK_DISABLED | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; + private static final int KEYGUARD_SYSUI_FLAGS = SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_DEVICE_DOZING + | SYSUI_STATE_OVERVIEW_DISABLED | SYSUI_STATE_HOME_DISABLED + | SYSUI_STATE_BACK_DISABLED | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED + | SYSUI_STATE_WAKEFULNESS_MASK; + + // If any of these SysUi flags (via QuickstepContract) is set, the device to be considered + // locked. + public static final int MASK_ANY_SYSUI_LOCKED = SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED + | SYSUI_STATE_DEVICE_DREAMING; private final TaskbarActivityContext mContext; private int mKeyguardSysuiFlags; private boolean mBouncerShowing; private NavbarButtonsViewController mNavbarButtonsViewController; private final KeyguardManager mKeyguardManager; - private boolean mIsScreenOff; - - private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mIsScreenOff = true; - } - }; public TaskbarKeyguardController(TaskbarActivityContext context) { mContext = context; @@ -45,10 +51,16 @@ public class TaskbarKeyguardController { public void init(NavbarButtonsViewController navbarButtonUIController) { mNavbarButtonsViewController = navbarButtonUIController; - mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } public void updateStateForSysuiFlags(int systemUiStateFlags) { + int interestingKeyguardFlags = systemUiStateFlags & KEYGUARD_SYSUI_FLAGS; + if (interestingKeyguardFlags == mKeyguardSysuiFlags) { + // No change in keyguard relevant flags + return; + } + mKeyguardSysuiFlags = interestingKeyguardFlags; + boolean bouncerShowing = (systemUiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0; boolean keyguardShowing = (systemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) != 0; @@ -56,25 +68,22 @@ public class TaskbarKeyguardController { (systemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0; boolean dozing = (systemUiStateFlags & SYSUI_STATE_DEVICE_DOZING) != 0; - int interestingKeyguardFlags = systemUiStateFlags & KEYGUARD_SYSUI_FLAGS; - if (interestingKeyguardFlags == mKeyguardSysuiFlags) { - return; - } - mKeyguardSysuiFlags = interestingKeyguardFlags; mBouncerShowing = bouncerShowing; mNavbarButtonsViewController.setKeyguardVisible(keyguardShowing || dozing, keyguardOccluded); updateIconsForBouncer(); - } - public boolean isScreenOff() { - return mIsScreenOff; - } + boolean asleepOrGoingToSleep = (systemUiStateFlags & SYSUI_STATE_AWAKE) == 0; + boolean closeFloatingViews = keyguardShowing || asleepOrGoingToSleep; - public void setScreenOn() { - mIsScreenOff = false; + if (closeFloatingViews) { + // animate the closing of the views, unless the screen is already asleep. + boolean animateViewClosing = + (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_ASLEEP; + AbstractFloatingView.closeOpenViews(mContext, animateViewClosing, TYPE_ALL); + } } /** @@ -82,17 +91,18 @@ public class TaskbarKeyguardController { */ private void updateIconsForBouncer() { boolean disableBack = (mKeyguardSysuiFlags & SYSUI_STATE_BACK_DISABLED) != 0; - boolean disableRecent = (mKeyguardSysuiFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0; - boolean disableHome = (mKeyguardSysuiFlags & SYSUI_STATE_HOME_DISABLED) != 0; - boolean onlyBackEnabled = !disableBack && disableRecent && disableHome; - - boolean showBackForBouncer = onlyBackEnabled && - mKeyguardManager.isDeviceSecure() && - mBouncerShowing; + boolean showBackForBouncer = + !disableBack && mKeyguardManager.isDeviceSecure() && mBouncerShowing; mNavbarButtonsViewController.setBackForBouncer(showBackForBouncer); } - public void onDestroy() { - mContext.unregisterReceiver(mScreenOffReceiver); + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarKeyguardController:"); + + pw.println(prefix + "\tmKeyguardSysuiFlags=" + QuickStepContract.getSystemUiStateString( + mKeyguardSysuiFlags)); + pw.println(prefix + "\tmBouncerShowing=" + mBouncerShowing); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index be5ab55353..c9e7df45a1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -15,63 +15,166 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE; -import static com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION; 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.animation.Interpolators.EMPHASIZED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; +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; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.os.SystemClock; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import com.android.launcher3.BaseQuickstepLauncher; +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherState; +import com.android.launcher3.QuickstepTransitionManager; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.statemanager.StateManager; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.quickstep.AnimatedFloat; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.views.RecentsView; +import com.android.systemui.animation.ViewRootSync; import com.android.systemui.shared.recents.model.ThumbnailData; +import java.io.PrintWriter; import java.util.HashMap; -import java.util.function.Consumer; -import java.util.function.Supplier; +import java.util.StringJoiner; /** * Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate * the task bar accordingly. */ - public class TaskbarLauncherStateController { +public class TaskbarLauncherStateController { + private static final String TAG = TaskbarLauncherStateController.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** Launcher activity is resumed and focused. */ public static final int FLAG_RESUMED = 1 << 0; - public static final int FLAG_RECENTS_ANIMATION_RUNNING = 1 << 1; - public static final int FLAG_TRANSITION_STATE_RUNNING = 1 << 2; + /** + * A external transition / animation is running that will result in FLAG_RESUMED being set. + **/ + public static final int FLAG_TRANSITION_TO_RESUMED = 1 << 1; + + /** + * Set while the launcher state machine is performing a state transition, see {@link + * StateManager.StateListener}. + */ + public static final int FLAG_LAUNCHER_IN_STATE_TRANSITION = 1 << 2; + + /** + * Whether the screen is currently on, or is transitioning to be on. + * + * This is cleared as soon as the screen begins to transition off. + */ + private static final int FLAG_AWAKE = 1 << 3; + + /** + * Captures whether the launcher was active at the time the FLAG_AWAKE was cleared. + * Always cleared when FLAG_AWAKE is set. + *

+ * FLAG_RESUMED will be cleared when the device is asleep, since all apps get paused at this + * point. Thus, this flag indicates whether the launcher will be shown when the device wakes up + * again. + */ + private static final int FLAG_LAUNCHER_WAS_ACTIVE_WHILE_AWAKE = 1 << 4; + + /** + * Whether the device is currently locked. + *

    + *
  • While locked, the taskbar is always stashed.
  • + *
  • Navbar animations on FLAG_DEVICE_LOCKED transitions will get special treatment.
  • + *
+ */ + private static final int FLAG_DEVICE_LOCKED = 1 << 5; + + /** + * Whether the complete taskbar is completely hidden (neither visible stashed or unstashed). + * This is tracked to allow a nice transition of the taskbar before SysUI forces it away by + * hiding the inset. + * + * This flag is predominanlty set while FLAG_DEVICE_LOCKED is set, thus the taskbar's invisible + * resting state while hidden is stashed. + */ + private static final int FLAG_TASKBAR_HIDDEN = 1 << 6; + + private static final int FLAGS_LAUNCHER_ACTIVE = FLAG_RESUMED | FLAG_TRANSITION_TO_RESUMED; /** Equivalent to an int with all 1s for binary operation purposes */ private static final int FLAGS_ALL = ~0; - private final AnimatedFloat mIconAlignmentForResumedState = - new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition); - private final AnimatedFloat mIconAlignmentForGestureState = - new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition); - private final AnimatedFloat mIconAlignmentForLauncherState = - new AnimatedFloat(this::onIconAlignmentRatioChangedForStateTransition); + private static final float TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT = 0.33f; + private static final float TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT = 0.33f; + private static final float TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT = 0.25f; + + /** + * Delay for the taskbar fade-in. + * + * Helps to avoid visual noise when unlocking successfully via SFPS, and the device transitions + * to launcher directly. The delay avoids the navbar to become briefly visible. The duration + * is the same as in SysUI, see http://shortn/_uNSbDoRUSr. + */ + private static final long TASKBAR_SHOW_DELAY_MS = 250; + + private final AnimatedFloat mIconAlignment = + new AnimatedFloat(this::onIconAlignmentRatioChanged); private TaskbarControllers mControllers; private AnimatedFloat mTaskbarBackgroundAlpha; - private MultiValueAlpha.AlphaProperty mIconAlphaForHome; - private BaseQuickstepLauncher mLauncher; + private AnimatedFloat mTaskbarAlpha; + private AnimatedFloat mTaskbarCornerRoundness; + private MultiProperty mIconAlphaForHome; + private QuickstepLauncher mLauncher; private Integer mPrevState; private int mState; private LauncherState mLauncherState = LauncherState.NORMAL; - private boolean mIsAnimatingToLauncherViaGesture; - private boolean mIsAnimatingToLauncherViaResume; + // Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds). + private long mLastUnlockTimeMs = 0; + + private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener; + + private boolean mIsAnimatingToLauncher; + + private boolean mShouldDelayLauncherStateAnim; + + // We skip any view synchronizations during init/destroy. + private boolean mCanSyncViews; + + private boolean mIsQsbInline; + + private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = + new DeviceProfile.OnDeviceProfileChangeListener() { + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + 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); + } + mIsQsbInline = dp.isQsbInline; + TaskbarLauncherStateController.this.updateIconAlphaForHome( + mIconAlphaForHome.getValue()); + } + }; private final StateManager.StateListener mStateListener = new StateManager.StateListener() { @@ -79,55 +182,78 @@ import java.util.function.Supplier; @Override public void onStateTransitionStart(LauncherState toState) { if (toState != mLauncherState) { - // Treat FLAG_TRANSITION_STATE_RUNNING as a changed flag even if a previous - // state transition was already running, so we update the new target. - mPrevState &= ~FLAG_TRANSITION_STATE_RUNNING; + // Treat FLAG_LAUNCHER_IN_STATE_TRANSITION as a changed flag even if a + // previous state transition was already running, so we update the new + // target. + mPrevState &= ~FLAG_LAUNCHER_IN_STATE_TRANSITION; mLauncherState = toState; } - updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, true); - applyState(); + updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true); + if (!mShouldDelayLauncherStateAnim) { + if (toState == LauncherState.NORMAL) { + applyState(QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION); + } else { + applyState(); + } + } } @Override public void onStateTransitionComplete(LauncherState finalState) { mLauncherState = finalState; - updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false); + updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false); applyState(); + boolean disallowLongClick = finalState == LauncherState.OVERVIEW_SPLIT_SELECT; + com.android.launcher3.taskbar.Utilities.setOverviewDragState( + mControllers, finalState.disallowTaskbarGlobalDrag(), + disallowLongClick, finalState.allowTaskbarInitialSplitSelection()); } }; - public void init(TaskbarControllers controllers, BaseQuickstepLauncher launcher) { + public void init(TaskbarControllers controllers, QuickstepLauncher launcher) { + mCanSyncViews = false; + mControllers = controllers; mLauncher = launcher; + mIsQsbInline = mLauncher.getDeviceProfile().isQsbInline; + mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController .getTaskbarBackgroundAlpha(); - MultiValueAlpha taskbarIconAlpha = mControllers.taskbarViewController.getTaskbarIconAlpha(); - mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME); - mIconAlphaForHome.setConsumer( - (Consumer) alpha -> mLauncher.getHotseat().setIconsAlpha(alpha > 0 ? 0 : 1)); + mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha(); + mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness(); + mIconAlphaForHome = mControllers.taskbarViewController + .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME); - mIconAlignmentForResumedState.finishAnimation(); - onIconAlignmentRatioChangedForAppAndHomeTransition(); + resetIconAlignment(); mLauncher.getStateManager().addStateListener(mStateListener); - - // Initialize to the current launcher state - updateStateForFlag(FLAG_RESUMED, launcher.hasBeenResumed()); mLauncherState = launcher.getStateManager().getState(); applyState(0); + + mCanSyncViews = true; + mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); } public void onDestroy() { - mIconAlignmentForResumedState.finishAnimation(); - mIconAlignmentForGestureState.finishAnimation(); - mIconAlignmentForLauncherState.finishAnimation(); + mCanSyncViews = false; - mIconAlphaForHome.setConsumer(null); + mIconAlignment.finishAnimation(); + + Log.d("b/260135164", "onDestroy - updateIconAlphaForHome(1)"); mLauncher.getHotseat().setIconsAlpha(1f); mLauncher.getStateManager().removeStateListener(mStateListener); + + mCanSyncViews = true; + mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); } + /** + * Creates a transition animation to the launcher activity. + * + * Warning: the resulting animation must be played, since this method has side effects on this + * controller's state. + */ public Animator createAnimToLauncher(@NonNull LauncherState toState, @NonNull RecentsAnimationCallbacks callbacks, long duration) { // If going to overview, stash the task bar @@ -138,24 +264,69 @@ import java.util.function.Supplier; TaskbarStashController stashController = mControllers.taskbarStashController; stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, toState.isTaskbarStashed(mLauncher)); + if (DEBUG) { + Log.d(TAG, "createAnimToLauncher - FLAG_IN_APP: " + false); + } stashController.updateStateForFlag(FLAG_IN_APP, false); - updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, true); - animatorSet.play(stashController.applyStateWithoutStart(duration)); + updateStateForFlag(FLAG_TRANSITION_TO_RESUMED, true); + animatorSet.play(stashController.createApplyStateAnimator(duration)); animatorSet.play(applyState(duration, false)); - TaskBarRecentsAnimationListener listener = new TaskBarRecentsAnimationListener(callbacks); - callbacks.addListener(listener); - RecentsView recentsView = mLauncher.getOverviewPanel(); - recentsView.setTaskLaunchListener(() -> { - listener.endGestureStateOverride(true); - callbacks.removeListener(listener); - }); + if (mTaskBarRecentsAnimationListener != null) { + mTaskBarRecentsAnimationListener.endGestureStateOverride( + !mLauncher.isInState(LauncherState.OVERVIEW)); + } + mTaskBarRecentsAnimationListener = new TaskBarRecentsAnimationListener(callbacks); + callbacks.addListener(mTaskBarRecentsAnimationListener); + ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() -> + mTaskBarRecentsAnimationListener.endGestureStateOverride(true)); return animatorSet; } public boolean isAnimatingToLauncher() { - return mIsAnimatingToLauncherViaResume || mIsAnimatingToLauncherViaGesture; + return mIsAnimatingToLauncher; + } + + public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) { + if (!shouldDelayLauncherStateAnim && mShouldDelayLauncherStateAnim) { + // Animate the animation we have delayed immediately. This is usually triggered when + // the user has released their finger. + applyState(); + } + mShouldDelayLauncherStateAnim = shouldDelayLauncherStateAnim; + } + + /** SysUI flags updated, see QuickStepContract.SYSUI_STATE_* values. */ + public void updateStateForSysuiFlags(int systemUiStateFlags, boolean skipAnim) { + final boolean prevIsAwake = hasAnyFlag(FLAG_AWAKE); + final boolean currIsAwake = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_AWAKE); + + updateStateForFlag(FLAG_AWAKE, currIsAwake); + if (prevIsAwake != currIsAwake) { + // The screen is switching between on/off. When turning off, capture whether the + // launcher is active and memoize this state. + updateStateForFlag(FLAG_LAUNCHER_WAS_ACTIVE_WHILE_AWAKE, + prevIsAwake && hasAnyFlag(FLAGS_LAUNCHER_ACTIVE)); + } + + boolean isDeviceLocked = hasAnyFlag(systemUiStateFlags, MASK_ANY_SYSUI_LOCKED); + updateStateForFlag(FLAG_DEVICE_LOCKED, isDeviceLocked); + + // Taskbar is hidden whenever the device is dreaming. The dreaming state includes the + // interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in + // when the device is asleep, the second condition extends ensures that the transition from + // and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar + // hide/reveal animation timings. + boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING) + || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE; + updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden); + + if (skipAnim) { + applyState(0); + } else { + applyState(); + } } /** @@ -163,7 +334,7 @@ import java.util.function.Supplier; * * Note that this only updates the flag. {@link #applyState()} needs to be called separately. * - * @param flag The flag to update. + * @param flag The flag to update. * @param enabled Whether to enable the flag */ public void updateStateForFlag(int flag, boolean enabled) { @@ -183,7 +354,7 @@ import java.util.function.Supplier; } public void applyState() { - applyState(TASKBAR_STASH_DURATION); + applyState(mControllers.taskbarStashController.getStashDuration()); } public void applyState(long duration) { @@ -191,14 +362,30 @@ import java.util.function.Supplier; } public Animator applyState(boolean start) { - return applyState(TASKBAR_STASH_DURATION, start); + return applyState(mControllers.taskbarStashController.getStashDuration(), start); } public Animator applyState(long duration, boolean start) { + if (mControllers.taskbarActivityContext.isDestroyed()) { + return null; + } Animator animator = null; if (mPrevState == null || mPrevState != mState) { // If this is our initial state, treat all flags as changed. int changedFlags = mPrevState == null ? FLAGS_ALL : mPrevState ^ mState; + + if (DEBUG) { + String stateString; + if (mPrevState == null) { + stateString = getStateString(mState) + "(initial update)"; + } else { + stateString = formatFlagChange(mState, mPrevState, + TaskbarLauncherStateController::getStateString); + } + Log.d(TAG, "applyState: " + stateString + + ", duration: " + duration + + ", start: " + start); + } mPrevState = mState; animator = onStateChangeApplied(changedFlags, duration, start); } @@ -206,117 +393,235 @@ import java.util.function.Supplier; } private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) { + final boolean isInLauncher = isInLauncher(); + final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat(); + final float toAlignment = isIconAlignedWithHotseat ? 1 : 0; + boolean handleOpenFloatingViews = false; + if (DEBUG) { + Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher + + ", mLauncherState: " + mLauncherState + + ", toAlignment: " + toAlignment); + } AnimatorSet animatorSet = new AnimatorSet(); - // Add the state animation first to ensure FLAG_IN_STASHED_LAUNCHER_STATE is set and we can - // determine whether goingToUnstashedLauncherStateChanged. - boolean wasGoingToUnstashedLauncherState = goingToUnstashedLauncherState(); - if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_RUNNING)) { - boolean committed = !hasAnyFlag(FLAG_TRANSITION_STATE_RUNNING); - playStateTransitionAnim(animatorSet, duration, committed); + if (hasAnyFlag(changedFlags, FLAG_LAUNCHER_IN_STATE_TRANSITION)) { + boolean launcherTransitionCompleted = !hasAnyFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION); + playStateTransitionAnim(animatorSet, duration, launcherTransitionCompleted); - if (committed && mLauncherState == LauncherState.QUICK_SWITCH) { + if (launcherTransitionCompleted + && mLauncherState == LauncherState.QUICK_SWITCH_FROM_HOME) { // We're about to be paused, set immediately to ensure seamless handoff. updateStateForFlag(FLAG_RESUMED, false); applyState(0 /* duration */); } + if (mLauncherState == LauncherState.NORMAL) { + // We're changing state to home, should close open popups e.g. Taskbar AllApps + handleOpenFloatingViews = true; + } } - boolean goingToUnstashedLauncherStateChanged = wasGoingToUnstashedLauncherState - != goingToUnstashedLauncherState(); - boolean launcherStateChangedDuringAnimToResumeAlignment = - mIconAlignmentForResumedState.isAnimating() && goingToUnstashedLauncherStateChanged; - if (hasAnyFlag(changedFlags, FLAG_RESUMED) - || launcherStateChangedDuringAnimToResumeAlignment) { - boolean isResumed = isResumed(); - float toAlignmentForResumedState = isResumed && goingToUnstashedLauncherState() ? 1 : 0; - // If we're already animating to the value, just leave it be instead of restarting it. - if (!mIconAlignmentForResumedState.isAnimatingToValue(toAlignmentForResumedState)) { - ObjectAnimator resumeAlignAnim = mIconAlignmentForResumedState - .animateToValue(toAlignmentForResumedState) - .setDuration(duration); + if (hasAnyFlag(changedFlags, FLAGS_LAUNCHER_ACTIVE | FLAG_AWAKE)) { + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mIsAnimatingToLauncher = isInLauncher; - resumeAlignAnim.addListener(new AnimatorListenerAdapter() { + TaskbarStashController stashController = + mControllers.taskbarStashController; + if (DEBUG) { + Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher); + } + stashController.updateStateForFlag(FLAG_IN_APP, !isInLauncher); + stashController.applyState(duration); + } + + @Override + public void onAnimationEnd(Animator animation) { + mIsAnimatingToLauncher = false; + } + }); + + // Handle closing open popups when going home/overview + handleOpenFloatingViews = true; + } + + if (handleOpenFloatingViews && isInLauncher) { + AbstractFloatingView.closeAllOpenViews(mControllers.taskbarActivityContext); + } + + if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { + // Take note of the current time, as the taskbar is made visible again. + mLastUnlockTimeMs = SystemClock.elapsedRealtime(); + } + + boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN); + float taskbarAlpha = isHidden ? 0 : 1; + if (mTaskbarAlpha.isAnimating() || mTaskbarAlpha.value != taskbarAlpha) { + Animator taskbarVisibility = mTaskbarAlpha.animateToValue(taskbarAlpha); + + taskbarVisibility.setDuration(duration); + if (isHidden) { + // Stash the transient taskbar once the taskbar is not visible. This reduces + // visual noise when unlocking the device afterwards. + animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mIsAnimatingToLauncherViaResume = false; - } - - @Override - public void onAnimationStart(Animator animation) { - mIsAnimatingToLauncherViaResume = isResumed; - TaskbarStashController stashController = mControllers.taskbarStashController; - stashController.updateStateForFlag(FLAG_IN_APP, !isResumed); - stashController.applyState(duration); + stashController.updateAndAnimateTransientTaskbar( + /* stash */ true, /* duration */ 0); } }); - animatorSet.play(resumeAlignAnim); + } else { + // delay the fade in animation a bit to reduce visual noise when waking up a device + // with a fingerprint reader. This should only be done when the device was woken + // up via fingerprint reader, however since this information is currently not + // available, opting to always delay the fade-in a bit. + long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs; + taskbarVisibility.setStartDelay( + Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs)); } + animatorSet.play(taskbarVisibility); } + float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1; - boolean launcherStateChangedDuringAnimToGestureAlignment = - mIconAlignmentForGestureState.isAnimating() && goingToUnstashedLauncherStateChanged; - if (hasAnyFlag(changedFlags, FLAG_RECENTS_ANIMATION_RUNNING) - || launcherStateChangedDuringAnimToGestureAlignment) { - boolean isRecentsAnimationRunning = isRecentsAnimationRunning(); - float toAlignmentForGestureState = isRecentsAnimationRunning - && goingToUnstashedLauncherState() ? 1 : 0; - // If we're already animating to the value, just leave it be instead of restarting it. - if (!mIconAlignmentForGestureState.isAnimatingToValue(toAlignmentForGestureState)) { - Animator gestureAlignAnim = mIconAlignmentForGestureState - .animateToValue(toAlignmentForGestureState); - if (isRecentsAnimationRunning) { - gestureAlignAnim.setDuration(duration); - } - gestureAlignAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mIsAnimatingToLauncherViaGesture = false; - } - - @Override - public void onAnimationStart(Animator animation) { - mIsAnimatingToLauncherViaGesture = isRecentsAnimationRunning(); - } - }); - animatorSet.play(gestureAlignAnim); + // Don't animate if background has reached desired value. + if (mTaskbarBackgroundAlpha.isAnimating() + || mTaskbarBackgroundAlpha.value != backgroundAlpha) { + mTaskbarBackgroundAlpha.cancelAnimation(); + if (DEBUG) { + Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - " + + mTaskbarBackgroundAlpha.value + + " -> " + backgroundAlpha + ": " + duration); } + + boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat; + boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat; + boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat; + + float startDelay = 0; + // We want to delay the background from fading in so that the icons have time to move + // into the bounds of the background before it appears. + if (isInLauncherIconNotAligned) { + startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT; + } else if (notInLauncherIconNotAligned) { + startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT; + } + float newDuration = duration - startDelay; + if (isInLauncherIconIsAligned) { + // Make the background fade out faster so that it is gone by the time the + // icons move outside of the bounds of the background. + newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT; + } + Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha + .animateToValue(backgroundAlpha) + .setDuration((long) newDuration); + taskbarBackgroundAlpha.setStartDelay((long) startDelay); + animatorSet.play(taskbarBackgroundAlpha); } - if (hasAnyFlag(changedFlags, FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING)) { - boolean goingToLauncher = hasAnyFlag(FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING); - animatorSet.play(mTaskbarBackgroundAlpha.animateToValue(goingToLauncher ? 0 : 1) - .setDuration(duration)); + float cornerRoundness = isInLauncher ? 0 : 1; + + // Don't animate if corner roundness has reached desired value. + if (mTaskbarCornerRoundness.isAnimating() + || mTaskbarCornerRoundness.value != cornerRoundness) { + mTaskbarCornerRoundness.cancelAnimation(); + if (DEBUG) { + Log.d(TAG, "onStateChangeApplied - taskbarCornerRoundness - " + + mTaskbarCornerRoundness.value + + " -> " + cornerRoundness + ": " + duration); + } + animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness)); } + // Keep isUnlockTransition in sync with its counterpart in + // TaskbarStashController#createAnimToIsStashed. + boolean isUnlockTransition = + hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED); + if (isUnlockTransition) { + // When transitioning to unlocked, ensure the hotseat is fully visible from the + // beginning. The hotseat itself is animated by LauncherUnlockAnimationController. + mIconAlignment.cancelAnimation(); + // updateValue ensures onIconAlignmentRatioChanged will be called if there is an actual + // change in value + mIconAlignment.updateValue(toAlignment); + } else if (mIconAlignment.isAnimatingToValue(toAlignment) + || mIconAlignment.isSettledOnValue(toAlignment)) { + // Already at desired value, but make sure we run the callback at the end. + animatorSet.addListener(AnimatorListeners.forEndCallback( + this::onIconAlignmentRatioChanged)); + } else { + mIconAlignment.cancelAnimation(); + ObjectAnimator iconAlignAnim = mIconAlignment + .animateToValue(toAlignment) + .setDuration(duration); + if (DEBUG) { + Log.d(TAG, "onStateChangeApplied - iconAlignment - " + + mIconAlignment.value + + " -> " + toAlignment + ": " + duration); + } + animatorSet.play(iconAlignAnim); + } + + animatorSet.setInterpolator(EMPHASIZED); + if (start) { animatorSet.start(); } return animatorSet; } - /** Returns whether we're going to a state where taskbar icons should align with launcher. */ - private boolean goingToUnstashedLauncherState() { - return !mControllers.taskbarStashController.isInStashedLauncherState(); + /** + * Whether the taskbar is aligned with the hotseat in the current/target launcher state. + * + * This refers to the intended state - a transition to this state might be in progress. + */ + public boolean isTaskbarAlignedWithHotseat() { + return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); + } + + /** + * Returns if icons should be aligned to hotseat in the current transition + */ + public boolean isIconAlignedWithHotseat() { + if (isInLauncher()) { + boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); + boolean willStashVisually = isInStashedState + && mControllers.taskbarStashController.supportsVisualStashing(); + boolean isTaskbarAlignedWithHotseat = + mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); + return isTaskbarAlignedWithHotseat && !willStashVisually; + } else { + return false; + } + } + + /** + * Returns if the current Launcher state has hotseat on top of other elemnets. + */ + public boolean isInHotseatOnTopStates() { + return mLauncherState != LauncherState.ALL_APPS; + } + + boolean isInOverview() { + return mLauncherState == LauncherState.OVERVIEW; } private void playStateTransitionAnim(AnimatorSet animatorSet, long duration, boolean committed) { boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); - float toAlignment = mLauncherState.isTaskbarAlignedWithHotseat(mLauncher) ? 1 : 0; - - TaskbarStashController controller = mControllers.taskbarStashController; - controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState); - Animator stashAnimator = controller.applyStateWithoutStart(duration); + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState); + Animator stashAnimator = stashController.createApplyStateAnimator(duration); if (stashAnimator != null) { stashAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (isInStashedState && committed) { // Reset hotseat alpha to default + Log.d("b/260135164", + "playStateTransitionAnim#onAnimationEnd - setIconsAlpha(1)"); mLauncher.getHotseat().setIconsAlpha(1); } } @@ -324,61 +629,91 @@ import java.util.function.Supplier; @Override public void onAnimationStart(Animator animation) { if (mLauncher.getHotseat().getIconsAlpha() > 0) { - mIconAlphaForHome.setValue(mLauncher.getHotseat().getIconsAlpha()); + updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha()); } } }); animatorSet.play(stashAnimator); } - // If we're already animating to the value, just leave it be instead of restarting it. - if (!mIconAlignmentForLauncherState.isAnimatingToValue(toAlignment)) { - animatorSet.play(mIconAlignmentForLauncherState.animateToValue(toAlignment) - .setDuration(duration)); + // Translate back to 0 at a shorter or same duration as the icon alignment animation. + // This ensures there is no jump after switching to hotseat, e.g. when swiping up from + // overview to home. When not in app, we do duration / 2 just to make it feel snappier. + long resetDuration = mControllers.taskbarStashController.isInApp() + ? duration + : duration / 2; + boolean shouldReset = + mControllers.taskbarTranslationController.shouldResetBackToZero(resetDuration); + boolean goingToLauncher = isAnimatingToLauncher(); + boolean isNormalState = mLauncherState == LauncherState.NORMAL; + // Taskbar should always reset when animating to launcher in normal state to ensure there + // is no jump during the handoff to the hotseat. + if ((goingToLauncher && isNormalState) + || (shouldReset && (goingToLauncher || isNormalState))) { + animatorSet.play(mControllers.taskbarTranslationController + .createAnimToResetTranslation(resetDuration)); } } - private boolean isResumed() { - return (mState & FLAG_RESUMED) != 0; - } - - private boolean isRecentsAnimationRunning() { - return (mState & FLAG_RECENTS_ANIMATION_RUNNING) != 0; - } - - private void onIconAlignmentRatioChangedForStateTransition() { - if (!isResumed()) { - return; + /** Whether the launcher is considered active. */ + private boolean isInLauncher() { + if (hasAnyFlag(FLAG_AWAKE)) { + return hasAnyFlag(FLAGS_LAUNCHER_ACTIVE); + } else { + return hasAnyFlag(FLAG_LAUNCHER_WAS_ACTIVE_WHILE_AWAKE); } - onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioForLauncherState); } - private void onIconAlignmentRatioChangedForAppAndHomeTransition() { - onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioBetweenAppAndHome); + /** + * Resets and updates the icon alignment. + */ + protected void resetIconAlignment() { + mIconAlignment.finishAnimation(); + onIconAlignmentRatioChanged(); } - private void onIconAlignmentRatioChanged(Supplier alignmentSupplier) { - if (mControllers == null) { - return; - } - float alignment = alignmentSupplier.get(); + private void onIconAlignmentRatioChanged() { + float currentValue = mIconAlphaForHome.getValue(); + boolean taskbarWillBeVisible = mIconAlignment.value < 1; + boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0) + || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0); + mControllers.taskbarViewController.setLauncherIconAlignment( - alignment, mLauncher.getDeviceProfile()); - + mIconAlignment.value, mLauncher.getDeviceProfile()); + mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value); // Switch taskbar and hotseat in last frame - setTaskbarViewVisible(alignment < 1); + updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0); + + // Sync the first frame where we swap taskbar and hotseat. + if (firstFrameVisChanged && mCanSyncViews && !Utilities.isRunningInTestHarness()) { + ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(), + mControllers.taskbarActivityContext.getDragLayer(), + () -> { + }); + } } - private float getCurrentIconAlignmentRatioBetweenAppAndHome() { - return Math.max(mIconAlignmentForResumedState.value, mIconAlignmentForGestureState.value); - } - - private float getCurrentIconAlignmentRatioForLauncherState() { - return mIconAlignmentForLauncherState.value; - } - - private void setTaskbarViewVisible(boolean isVisible) { - mIconAlphaForHome.setValue(isVisible ? 1 : 0); + private void updateIconAlphaForHome(float alpha) { + if (mControllers.taskbarActivityContext.isDestroyed()) { + Log.e("b/260135164", "updateIconAlphaForHome is called after Taskbar is destroyed", + new Exception()); + return; + } + mIconAlphaForHome.setValue(alpha); + boolean hotseatVisible = alpha == 0 + || (!mControllers.uiController.isHotseatIconOnTopWhenAligned() + && mIconAlignment.value > 0); + /* + * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets + * should not be visible at the same time. + */ + Log.d("b/260135164", + "updateIconAlphaForHome - setIconsAlpha(" + (hotseatVisible ? 1 : 0) + + "), isTaskbarPresent: " + mLauncher.getDeviceProfile().isTaskbarPresent); + mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0); + if (mIsQsbInline) { + mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0); + } } private final class TaskBarRecentsAnimationListener implements @@ -391,7 +726,8 @@ import java.util.function.Supplier; @Override public void onRecentsAnimationCanceled(HashMap thumbnailDatas) { - endGestureStateOverride(true); + boolean isInOverview = mLauncher.isInState(LauncherState.OVERVIEW); + endGestureStateOverride(!isInOverview); } @Override @@ -401,19 +737,55 @@ import java.util.function.Supplier; private void endGestureStateOverride(boolean finishedToApp) { mCallbacks.removeListener(this); + mTaskBarRecentsAnimationListener = null; + ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null); // Update the resumed state immediately to ensure a seamless handoff boolean launcherResumed = !finishedToApp; - updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, false); + updateStateForFlag(FLAG_TRANSITION_TO_RESUMED, false); updateStateForFlag(FLAG_RESUMED, launcherResumed); applyState(); - // Set this last because applyState() might also animate it. - mIconAlignmentForResumedState.cancelAnimation(); - mIconAlignmentForResumedState.updateValue(launcherResumed ? 1 : 0); TaskbarStashController controller = mControllers.taskbarStashController; + if (DEBUG) { + Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp); + } controller.updateStateForFlag(FLAG_IN_APP, finishedToApp); controller.applyState(); } } + + private static String getStateString(int flags) { + StringJoiner result = new StringJoiner("|"); + appendFlag(result, flags, FLAG_RESUMED, "resumed"); + appendFlag(result, flags, FLAG_TRANSITION_TO_RESUMED, "transition_to_resumed"); + appendFlag(result, flags, FLAG_LAUNCHER_IN_STATE_TRANSITION, + "launcher_in_state_transition"); + appendFlag(result, flags, FLAG_AWAKE, "awake"); + appendFlag(result, flags, FLAG_LAUNCHER_WAS_ACTIVE_WHILE_AWAKE, + "was_active_while_awake"); + appendFlag(result, flags, FLAG_DEVICE_LOCKED, "device_locked"); + return result.toString(); + } + + protected void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarLauncherStateController:"); + pw.println(String.format( + "%s\tmIconAlignment=%.2f", + prefix, + mIconAlignment.value)); + 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))); + pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState))); + pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState)); + pw.println(String.format( + "%s\tmIsAnimatingToLauncher=%b", + prefix, + mIsAnimatingToLauncher)); + pw.println(String.format( + "%s\tmShouldDelayLauncherStateAnim=%b", prefix, mShouldDelayLauncherStateAnim)); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 7386458d67..0280996cde 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -15,151 +15,272 @@ */ package com.android.launcher3.taskbar; +import static android.content.Context.RECEIVER_NOT_EXPORTED; +import static android.content.pm.PackageManager.FEATURE_PC; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; -import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN; import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; -import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; +import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; +import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; +import android.annotation.SuppressLint; +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; import android.net.Uri; import android.os.Handler; +import android.os.SystemProperties; import android.provider.Settings; +import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.DisplayController.Info; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.quickstep.RecentsActivity; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; +import java.io.PrintWriter; +import java.util.StringJoiner; + /** * Class to manage taskbar lifecycle */ -public class TaskbarManager implements DisplayController.DisplayInfoChangeListener, - SysUINavigationMode.NavigationModeChangeListener { +public class TaskbarManager { + private static final String TAG = "TaskbarManager"; + private static final boolean DEBUG = false; + + public static final boolean FLAG_HIDE_NAVBAR_WINDOW = SystemProperties + .getBoolean("persist.wm.debug.hide_navbar_window", false); private static final Uri USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor( Settings.Secure.USER_SETUP_COMPLETE); + private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor( + Settings.Secure.NAV_BAR_KIDS_MODE); + private final Context mContext; private final DisplayController mDisplayController; - private final SysUINavigationMode mSysUINavigationMode; private final TaskbarNavButtonController mNavButtonController; private final SettingsCache.OnChangeListener mUserSetupCompleteListener; + private final SettingsCache.OnChangeListener mNavBarKidsModeListener; private final ComponentCallbacks mComponentCallbacks; private final SimpleBroadcastReceiver mShutdownReceiver; // The source for this provider is set when Launcher is available - private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider = - new ScopedUnfoldTransitionProgressProvider(); + // We use 'non-destroyable' version here so the original provider won't be + // destroyed + // as it is tied to the activity lifecycle, not the taskbar lifecycle. + // It's destruction/creation will be managed by the activity. + private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider = new NonDestroyableScopedUnfoldTransitionProgressProvider(); + private NavigationMode mNavMode; private TaskbarActivityContext mTaskbarActivityContext; private StatefulActivity mActivity; /** - * Cache a copy here so we can initialize state whenever taskbar is recreated, since + * Cache a copy here so we can initialize state whenever taskbar is recreated, + * since * this class does not get re-initialized w/ new taskbars. */ private final TaskbarSharedState mSharedState = new TaskbarSharedState(); - private static final int CHANGE_FLAGS = - CHANGE_ACTIVE_SCREEN | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS; + /** + * We use WindowManager's ComponentCallbacks() for most of the config changes, + * however for + * navigation mode, that callback gets called too soon, before it's internal + * navigation mode + * reflects the current one. + * DisplayController's callback is delayed enough to get the correct nav mode + * value + * + * We also use density change here because DeviceProfile has had a chance to + * update it's state + * whereas density for component callbacks registered in this class don't update + * DeviceProfile. + * Confused? Me too. Make it less confusing (TODO: b/227669780) + * + * Flags used with {@link #mDispInfoChangeListener} + */ + private static final int CHANGE_FLAGS = CHANGE_NAVIGATION_MODE | CHANGE_DENSITY; + private final DisplayController.DisplayInfoChangeListener mDispInfoChangeListener; private boolean mUserUnlocked = false; + public static final int SYSTEM_ACTION_ID_TASKBAR = 499; + + /** + * For Taskbar broadcast intent filter. + */ + public static final String ACTION_SHOW_TASKBAR = "ACTION_SHOW_TASKBAR"; + + private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver = new SimpleBroadcastReceiver( + this::showTaskbarFromBroadcast); + + @SuppressLint("WrongConstant") public TaskbarManager(TouchInteractionService service) { mDisplayController = DisplayController.INSTANCE.get(service); - mSysUINavigationMode = SysUINavigationMode.INSTANCE.get(service); - Display display = - service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); + Display display = service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); mContext = Utilities.ATLEAST_S ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null) : null; mNavButtonController = new TaskbarNavButtonController(service, SystemUiProxy.INSTANCE.get(mContext), new Handler()); mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar(); + mNavBarKidsModeListener = isNavBarKidsMode -> recreateTaskbar(); + // TODO(b/227669780): Consolidate this w/ DisplayController callbacks mComponentCallbacks = new ComponentCallbacks() { private Configuration mOldConfig = mContext.getResources().getConfiguration(); @Override public void onConfigurationChanged(Configuration newConfig) { + debugWhyTaskbarNotDestroyed( + "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig); + DeviceProfile dp = mUserUnlocked + ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) + : null; int configDiff = mOldConfig.diff(newConfig); + int configDiffForRecreate = configDiff; int configsRequiringRecreate = ActivityInfo.CONFIG_ASSETS_PATHS - | ActivityInfo.CONFIG_LAYOUT_DIRECTION | ActivityInfo.CONFIG_UI_MODE; - if ((configDiff & configsRequiringRecreate) != 0) { - // Color has changed, recreate taskbar to reload background color & icons. + | ActivityInfo.CONFIG_LAYOUT_DIRECTION | ActivityInfo.CONFIG_UI_MODE + | ActivityInfo.CONFIG_SCREEN_SIZE; + if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0 + && mTaskbarActivityContext != null && dp != null + && !isPhoneMode(dp)) { + // Additional check since this callback gets fired multiple times w/o + // screen size changing, or when simply rotating the device. + // In the case of phone device rotation, we do want to call recreateTaskbar() + DeviceProfile oldDp = mTaskbarActivityContext.getDeviceProfile(); + boolean isOrientationChange = (configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0; + int oldWidth = isOrientationChange ? oldDp.heightPx : oldDp.widthPx; + int oldHeight = isOrientationChange ? oldDp.widthPx : oldDp.heightPx; + if (dp.widthPx == oldWidth && dp.heightPx == oldHeight) { + configDiffForRecreate &= ~ActivityInfo.CONFIG_SCREEN_SIZE; + } + } + if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) { + // Only recreate for theme changes, not other UI mode changes such as docking. + int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK); + int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK); + if (oldUiNightMode == newUiNightMode) { + configDiffForRecreate &= ~ActivityInfo.CONFIG_UI_MODE; + } + } + + debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() " + + "configDiffForRecreate=" + + Configuration.configurationDiffToString(configDiffForRecreate)); + if ((configDiffForRecreate & configsRequiringRecreate) != 0) { recreateTaskbar(); } else { // Config change might be handled without re-creating the taskbar if (mTaskbarActivityContext != null) { - DeviceProfile dp = mUserUnlocked - ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) - : null; - - if (dp != null && dp.isTaskbarPresent) { - mTaskbarActivityContext.updateDeviceProfile(dp.copy(mContext)); + if (dp != null && !isTaskbarPresent(dp)) { + destroyExistingTaskbar(); + } else { + if (dp != null && isTaskbarPresent(dp)) { + mTaskbarActivityContext.updateDeviceProfile(dp, mNavMode); + } + mTaskbarActivityContext.onConfigurationChanged(configDiff); } - mTaskbarActivityContext.onConfigurationChanged(configDiff); } } mOldConfig = newConfig; } @Override - public void onLowMemory() { } + public void onLowMemory() { + } }; mShutdownReceiver = new SimpleBroadcastReceiver(i -> destroyExistingTaskbar()); - - mDisplayController.addChangeListener(this); - mSysUINavigationMode.addModeChangeListener(this); + mDispInfoChangeListener = (context, info, flags) -> { + if ((flags & CHANGE_FLAGS) != 0) { + mNavMode = info.navigationMode; + recreateTaskbar(); + } + debugWhyTaskbarNotDestroyed("DisplayInfoChangeListener#" + + mDisplayController.getChangeFlagsString(flags)); + }; + mNavMode = mDisplayController.getInfo().navigationMode; + mDisplayController.addChangeListener(mDispInfoChangeListener); SettingsCache.INSTANCE.get(mContext).register(USER_SETUP_COMPLETE_URI, mUserSetupCompleteListener); + SettingsCache.INSTANCE.get(mContext).register(NAV_BAR_KIDS_MODE, + mNavBarKidsModeListener); mContext.registerComponentCallbacks(mComponentCallbacks); mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN); + UI_HELPER_EXECUTOR.execute(() -> { + mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast( + mContext, + 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); + }); + debugWhyTaskbarNotDestroyed("TaskbarManager created"); recreateTaskbar(); } - @Override - public void onNavigationModeChanged(Mode newMode) { - recreateTaskbar(); - } - - @Override - public void onDisplayInfoChanged(Context context, Info info, int flags) { - if ((flags & CHANGE_FLAGS) != 0) { - recreateTaskbar(); - } - } - private void destroyExistingTaskbar() { + debugWhyTaskbarNotDestroyed("destroyExistingTaskbar: " + mTaskbarActivityContext); if (mTaskbarActivityContext != null) { mTaskbarActivityContext.onDestroy(); - mTaskbarActivityContext = null; + if (!FLAG_HIDE_NAVBAR_WINDOW) { + mTaskbarActivityContext = null; + } } } + /** + * Show Taskbar upon receiving broadcast + */ + private void showTaskbarFromBroadcast(Intent intent) { + if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && mTaskbarActivityContext != null) { + mTaskbarActivityContext.showTaskbarFromBroadcast(); + } + } + + /** + * Displays a frame of the first Launcher reveal animation. + * + * This should be used to run a first Launcher reveal animation whose progress + * matches a swipe + * progress. + */ + public AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) { + return mTaskbarActivityContext == null + ? null + : mTaskbarActivityContext.createLauncherStartFromSuwAnim(duration); + } + /** * Called when the user is unlocked */ @@ -176,9 +297,18 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen * Sets a {@link StatefulActivity} to act as taskbar callback */ public void setActivity(@NonNull StatefulActivity activity) { + if (mActivity == activity) { + return; + } + if (mActivity != null) { + mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); + } mActivity = activity; - mUnfoldProgressProvider.setSourceProvider(getUnfoldTransitionProgressProviderForActivity( - activity)); + debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity); + mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); + UnfoldTransitionProgressProvider unfoldTransitionProgressProvider = getUnfoldTransitionProgressProviderForActivity( + activity); + mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider); if (mTaskbarActivityContext != null) { mTaskbarActivityContext.setUIController( @@ -187,23 +317,28 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen } /** - * Returns an {@link UnfoldTransitionProgressProvider} to use while the given StatefulActivity + * Returns an {@link UnfoldTransitionProgressProvider} to use while the given + * StatefulActivity * is active. */ private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity( StatefulActivity activity) { - if (activity instanceof BaseQuickstepLauncher) { - return ((BaseQuickstepLauncher) activity).getUnfoldTransitionProgressProvider(); + if (activity instanceof QuickstepLauncher) { + return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider(); } return null; } /** - * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active. + * Creates a {@link TaskbarUIController} to use while the given StatefulActivity + * is active. */ private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) { - if (activity instanceof BaseQuickstepLauncher) { - return new LauncherTaskbarUIController((BaseQuickstepLauncher) activity); + if (activity instanceof QuickstepLauncher) { + if (mTaskbarActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) { + return new DesktopTaskbarUIController((QuickstepLauncher) activity); + } + return new LauncherTaskbarUIController((QuickstepLauncher) activity); } if (activity instanceof RecentsActivity) { return new FallbackTaskbarUIController((RecentsActivity) activity); @@ -216,7 +351,9 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen */ public void clearActivity(@NonNull StatefulActivity activity) { if (mActivity == activity) { + mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); mActivity = null; + debugWhyTaskbarNotDestroyed("clearActivity"); if (mTaskbarActivityContext != null) { mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT); } @@ -224,26 +361,37 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen } } - private void recreateTaskbar() { + /** + * This method is called multiple times (ex. initial init, then when user + * unlocks) in which case + * we fully want to destroy an existing taskbar and create a new one. + * In other case (folding/unfolding) we don't need to remove and add window. + */ + @VisibleForTesting + public void recreateTaskbar() { + DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null; + destroyExistingTaskbar(); - DeviceProfile dp = - mUserUnlocked ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null; - - boolean isTaskBarEnabled = - FeatureFlags.ENABLE_TASKBAR.get() && dp != null && dp.isTaskbarPresent; - - SystemUiProxy sysui = SystemUiProxy.INSTANCE.get(mContext); - sysui.setTaskbarEnabled(isTaskBarEnabled); - if (!isTaskBarEnabled) { - sysui.notifyTaskbarStatus(/* visible */ false, /* stashed */ false); + boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp); + debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled + + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null) + + " FLAG_HIDE_NAVBAR_WINDOW=" + FLAG_HIDE_NAVBAR_WINDOW + + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent)); + if (!isTaskbarEnabled) { + SystemUiProxy.INSTANCE.get(mContext) + .notifyTaskbarStatus(/* visible */ false, /* stashed */ false); return; } - mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp.copy(mContext), - mNavButtonController, mUnfoldProgressProvider); - + if (mTaskbarActivityContext == null) { + mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp, mNavButtonController, + mUnfoldProgressProvider); + } else { + mTaskbarActivityContext.updateDeviceProfile(dp, mNavMode); + } mTaskbarActivityContext.init(mSharedState); + if (mActivity != null) { mTaskbarActivityContext.setUIController( createTaskbarUIControllerForActivity(mActivity)); @@ -251,12 +399,22 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen } public void onSystemUiFlagsChanged(int systemUiStateFlags) { + if (DEBUG) { + Log.d(TAG, "SysUI flags changed: " + formatFlagChange(systemUiStateFlags, + mSharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString)); + } mSharedState.sysuiStateFlags = systemUiStateFlags; if (mTaskbarActivityContext != null) { mTaskbarActivityContext.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */); } } + public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) { + if (mNavButtonController != null) { + mNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled); + } + } + /** * Sets the flag indicating setup UI is visible */ @@ -267,6 +425,27 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen } } + /** + * @return {@code true} if provided device profile isn't a large screen profile + * and we are using a single window for taskbar and navbar. + */ + public static boolean isPhoneMode(DeviceProfile deviceProfile) { + return TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW && deviceProfile.isPhone; + } + + /** + * @return {@code true} if {@link #isPhoneMode(DeviceProfile)} is true and we're + * using + * 3 button-nav + */ + public static boolean isPhoneButtonNavMode(TaskbarActivityContext context) { + return isPhoneMode(context.getDeviceProfile()) && context.isThreeButtonNav(); + } + + private boolean isTaskbarPresent(DeviceProfile deviceProfile) { + return FLAG_HIDE_NAVBAR_WINDOW || deviceProfile.isTaskbarPresent; + } + public void onRotationProposal(int rotation, boolean isValid) { if (mTaskbarActivityContext != null) { mTaskbarActivityContext.onRotationProposal(rotation, isValid); @@ -274,18 +453,24 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen } public void disableNavBarElements(int displayId, int state1, int state2, boolean animate) { + mSharedState.disableNavBarDisplayId = displayId; + mSharedState.disableNavBarState1 = state1; + mSharedState.disableNavBarState2 = state2; if (mTaskbarActivityContext != null) { mTaskbarActivityContext.disableNavBarElements(displayId, state1, state2, animate); } } public void onSystemBarAttributesChanged(int displayId, int behavior) { + mSharedState.systemBarAttrsDisplayId = displayId; + mSharedState.systemBarAttrsBehavior = behavior; if (mTaskbarActivityContext != null) { mTaskbarActivityContext.onSystemBarAttributesChanged(displayId, behavior); } } public void onNavButtonsDarkIntensityChanged(float darkIntensity) { + mSharedState.navButtonsDarkIntensity = darkIntensity; if (mTaskbarActivityContext != null) { mTaskbarActivityContext.onNavButtonsDarkIntensityChanged(darkIntensity); } @@ -295,11 +480,19 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen * Called when the manager is no longer needed */ public void destroy() { + debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()"); + if (mActivity != null) { + mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); + } + + UI_HELPER_EXECUTOR.execute( + () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext)); destroyExistingTaskbar(); - mDisplayController.removeChangeListener(this); - mSysUINavigationMode.removeModeChangeListener(this); + mDisplayController.removeChangeListener(mDispInfoChangeListener); SettingsCache.INSTANCE.get(mContext).unregister(USER_SETUP_COMPLETE_URI, mUserSetupCompleteListener); + SettingsCache.INSTANCE.get(mContext).unregister(NAV_BAR_KIDS_MODE, + mNavBarKidsModeListener); mContext.unregisterComponentCallbacks(mComponentCallbacks); mContext.unregisterReceiver(mShutdownReceiver); } @@ -307,4 +500,55 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen public @Nullable TaskbarActivityContext getCurrentActivityContext() { return mTaskbarActivityContext; } + + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarManager:"); + if (mTaskbarActivityContext == null) { + pw.println(prefix + "\tTaskbarActivityContext: null"); + } else { + mTaskbarActivityContext.dumpLogs(prefix + "\t", pw); + } + } + + /** Temp logs for b/254119092. */ + public void debugWhyTaskbarNotDestroyed(String debugReason) { + StringJoiner log = new StringJoiner("\n"); + log.add(debugReason); + + boolean activityTaskbarPresent = mActivity != null + && mActivity.getDeviceProfile().isTaskbarPresent; + boolean contextTaskbarPresent = mUserUnlocked + && LauncherAppState.getIDP(mContext).getDeviceProfile(mContext).isTaskbarPresent; + if (activityTaskbarPresent == contextTaskbarPresent) { + log.add("mActivity and mContext agree taskbarIsPresent=" + contextTaskbarPresent); + Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString()); + return; + } + + log.add("mActivity and mContext device profiles have different values, add more logs."); + + log.add("\tmActivity logs:"); + log.add("\t\tmActivity=" + mActivity); + if (mActivity != null) { + log.add("\t\tmActivity.getResources().getConfiguration()=" + + mActivity.getResources().getConfiguration()); + log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent=" + + activityTaskbarPresent); + } + log.add("\tmContext logs:"); + log.add("\t\tmContext=" + mContext); + log.add("\t\tmContext.getResources().getConfiguration()=" + + mContext.getResources().getConfiguration()); + if (mUserUnlocked) { + log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mContext).isTaskbarPresent=" + + contextTaskbarPresent); + } else { + log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked"); + } + + Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString()); + } + + private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged = dp -> debugWhyTaskbarNotDestroyed( + "mActivity onDeviceProfileChanged"); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 37a9b5e5b2..1ceb6533ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -21,6 +21,7 @@ import android.view.View; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.model.BgDataModel; 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.util.ComponentKey; @@ -28,18 +29,21 @@ import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; +import com.android.quickstep.RecentsModel; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.function.Predicate; /** * Launcher model Callbacks for rendering taskbar. */ public class TaskbarModelCallbacks implements - BgDataModel.Callbacks, LauncherBindableItemsContainer { + BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener { private final SparseArray mHotseatItems = new SparseArray<>(); private List mPredictedItems = Collections.emptyList(); @@ -50,8 +54,6 @@ public class TaskbarModelCallbacks implements // Initialized in init. private TaskbarControllers mControllers; - private boolean mBindInProgress = false; - public TaskbarModelCallbacks( TaskbarActivityContext context, TaskbarView container) { mContext = context; @@ -60,18 +62,28 @@ public class TaskbarModelCallbacks implements public void init(TaskbarControllers controllers) { mControllers = controllers; + if (mControllers.taskbarRecentAppsController.isEnabled()) { + RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this); + } + } + + /** + * Unregisters listeners in this class. + */ + public void unregisterListeners() { + RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener(); } @Override public void startBinding() { - mBindInProgress = true; + mContext.setBindingItems(true); mHotseatItems.clear(); mPredictedItems = Collections.emptyList(); } @Override public void finishBindingItems(IntSet pagesBoundFirst) { - mBindInProgress = false; + mContext.setBindingItems(false); commitItemsToUI(); } @@ -126,16 +138,16 @@ public class TaskbarModelCallbacks implements } @Override - public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { + public void bindWorkspaceComponentsRemoved(Predicate matcher) { if (handleItemsRemoved(matcher)) { commitItemsToUI(); } } - private boolean handleItemsRemoved(ItemInfoMatcher matcher) { + private boolean handleItemsRemoved(Predicate matcher) { boolean modified = false; for (int i = mHotseatItems.size() - 1; i >= 0; i--) { - if (matcher.matchesInfo(mHotseatItems.valueAt(i))) { + if (matcher.test(mHotseatItems.valueAt(i))) { modified = true; mHotseatItems.removeAt(i); } @@ -157,11 +169,13 @@ public class TaskbarModelCallbacks implements if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { mPredictedItems = item.items; commitItemsToUI(); + } else if (item.containerId == Favorites.CONTAINER_PREDICTION) { + mControllers.taskbarAllAppsController.setPredictedApps(item.items); } } private void commitItemsToUI() { - if (mBindInProgress) { + if (mContext.isBindingItems()) { return; } @@ -170,7 +184,6 @@ public class TaskbarModelCallbacks implements int predictionSize = mPredictedItems.size(); int predictionNextIndex = 0; - boolean isHotseatEmpty = true; for (int i = 0; i < hotseatItemInfos.length; i++) { hotseatItemInfos[i] = mHotseatItems.get(i); if (hotseatItemInfos[i] == null && predictionNextIndex < predictionSize) { @@ -178,22 +191,46 @@ public class TaskbarModelCallbacks implements hotseatItemInfos[i].screenId = i; predictionNextIndex++; } - if (hotseatItemInfos[i] != null) { - isHotseatEmpty = false; - } } + hotseatItemInfos = mControllers.taskbarRecentAppsController + .updateHotseatItemInfos(hotseatItemInfos); mContainer.updateHotseatItems(hotseatItemInfos); + mControllers.taskbarViewController.updateIconsBackground(); + } - final boolean finalIsHotseatEmpty = isHotseatEmpty; - mControllers.runAfterInit(() -> { - mControllers.taskbarStashController.updateStateForFlag( - TaskbarStashController.FLAG_STASHED_IN_APP_EMPTY, finalIsHotseatEmpty); - mControllers.taskbarStashController.applyState(); - }); + @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(mHotseatItems); } @Override public void bindDeepShortcutMap(HashMap deepShortcutMapCopy) { mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy); } + + @Override + public void bindAllApplications(AppInfo[] apps, int flags) { + mControllers.taskbarAllAppsController.setApps(apps, flags); + mControllers.taskbarRecentAppsController.setApps(apps); + } + + protected void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarModelCallbacks:"); + + pw.println(String.format("%s\thotseat items count=%s", prefix, mHotseatItems.size())); + if (mPredictedItems != null) { + pw.println( + String.format("%s\tpredicted items count=%s", prefix, mPredictedItems.size())); + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index d23336505a..610efebfcb 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -16,22 +16,41 @@ package com.android.launcher3.taskbar; - import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS; +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_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.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.os.Bundle; import android.os.Handler; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.View; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import com.android.launcher3.R; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.testing.TestLogging; -import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TaskUtils; import com.android.quickstep.TouchInteractionService; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -40,14 +59,24 @@ import java.lang.annotation.RetentionPolicy; * Handles all the functionality of the various buttons, making/routing the right calls into * launcher or sysui/system. */ -public class TaskbarNavButtonController { +public class TaskbarNavButtonController implements TaskbarControllers.LoggableTaskbarController { /** Allow some time in between the long press for back and recents. */ static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200; static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100; + private static final String TAG = TaskbarNavButtonController.class.getSimpleName(); private long mLastScreenPinLongPress; private boolean mScreenPinned; + private boolean mAssistantLongPressEnabled; + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarNavButtonController:"); + + pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress); + pw.println(prefix + "\tmScreenPinned=" + mScreenPinned); + } @Retention(RetentionPolicy.SOURCE) @IntDef(value = { @@ -56,6 +85,8 @@ public class TaskbarNavButtonController { BUTTON_RECENTS, BUTTON_IME_SWITCH, BUTTON_A11Y, + BUTTON_QUICK_SETTINGS, + BUTTON_NOTIFICATIONS, }) public @interface TaskbarButton {} @@ -65,6 +96,8 @@ public class TaskbarNavButtonController { static final int BUTTON_RECENTS = BUTTON_HOME << 1; static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1; static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1; + static final int BUTTON_QUICK_SETTINGS = BUTTON_A11Y << 1; + static final int BUTTON_NOTIFICATIONS = BUTTON_QUICK_SETTINGS << 1; private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS; private int mLongPressedButtons = 0; @@ -72,6 +105,7 @@ public class TaskbarNavButtonController { private final TouchInteractionService mService; private final SystemUiProxy mSystemUiProxy; private final Handler mHandler; + @Nullable private StatsLogManager mStatsLogManager; private final Runnable mResetLongPress = this::resetScreenUnpin; @@ -82,44 +116,89 @@ public class TaskbarNavButtonController { mHandler = handler; } - public void onButtonClick(@TaskbarButton int buttonType) { + public void onButtonClick(@TaskbarButton int buttonType, View view) { + // Provide the same haptic feedback that the system offers for virtual keys. + view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); switch (buttonType) { case BUTTON_BACK: + logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); executeBack(); break; case BUTTON_HOME: + logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); navigateHome(); break; case BUTTON_RECENTS: + logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP); navigateToOverview(); break; case BUTTON_IME_SWITCH: + logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); showIMESwitcher(); break; case BUTTON_A11Y: + logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP); notifyA11yClick(false /* longClick */); break; + case BUTTON_QUICK_SETTINGS: + showQuickSettings(); + break; + case BUTTON_NOTIFICATIONS: + showNotifications(); + break; } } - public boolean onButtonLongClick(@TaskbarButton int buttonType) { + public boolean onButtonLongClick(@TaskbarButton int buttonType, View view) { + // Provide the same haptic feedback that the system offers for virtual keys. + view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); switch (buttonType) { case BUTTON_HOME: + logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); startAssistant(); return true; case BUTTON_A11Y: + logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS); notifyA11yClick(true /* longClick */); return true; case BUTTON_BACK: + logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); + return backRecentsLongpress(buttonType); case BUTTON_RECENTS: - mLongPressedButtons |= buttonType; - return determineScreenUnpin(); + logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS); + return backRecentsLongpress(buttonType); case BUTTON_IME_SWITCH: default: return false; } } + public @StringRes int getButtonContentDescription(@TaskbarButton int buttonType) { + switch (buttonType) { + case BUTTON_HOME: + return R.string.taskbar_button_home; + case BUTTON_A11Y: + return R.string.taskbar_button_a11y; + case BUTTON_BACK: + return R.string.taskbar_button_back; + case BUTTON_IME_SWITCH: + return R.string.taskbar_button_ime_switcher; + case BUTTON_RECENTS: + return R.string.taskbar_button_recents; + case BUTTON_NOTIFICATIONS: + return R.string.taskbar_button_notifications; + case BUTTON_QUICK_SETTINGS: + return R.string.taskbar_button_quick_settings; + default: + return 0; + } + } + + private boolean backRecentsLongpress(@TaskbarButton int buttonType) { + mLongPressedButtons |= buttonType; + return determineScreenUnpin(); + } + /** * Checks if the user has long pressed back and recents buttons * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms @@ -166,7 +245,28 @@ public class TaskbarNavButtonController { mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0; } + public void init(TaskbarControllers taskbarControllers) { + mStatsLogManager = taskbarControllers.getTaskbarActivityContext().getStatsLogManager(); + } + + public void onDestroy() { + mStatsLogManager = null; + } + + public void setAssistantLongPressEnabled(boolean assistantLongPressEnabled) { + mAssistantLongPressEnabled = assistantLongPressEnabled; + } + + private void logEvent(StatsLogManager.LauncherEvent event) { + if (mStatsLogManager == null) { + Log.w(TAG, "No stats log manager to log taskbar button event"); + return; + } + mStatsLogManager.logger().log(event); + } + private void navigateHome() { + TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME); } @@ -175,6 +275,7 @@ public class TaskbarNavButtonController { return; } TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle"); + TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE); } @@ -195,11 +296,19 @@ public class TaskbarNavButtonController { } private void startAssistant() { - if (mScreenPinned) { + if (mScreenPinned || !mAssistantLongPressEnabled) { return; } Bundle args = new Bundle(); args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); mSystemUiProxy.startAssistant(args); } + + private void showQuickSettings() { + mSystemUiProxy.toggleNotificationPanel(); + } + + private void showNotifications() { + mSystemUiProxy.toggleNotificationPanel(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java index 952f597f72..90fcd37d2e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -15,35 +15,83 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.config.FeatureFlags.ENABLE_MATERIAL_U_POPUP; +import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; + +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.graphics.Point; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.View; + import androidx.annotation.NonNull; +import com.android.internal.logging.InstanceId; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.dot.FolderDotInfo; +import com.android.launcher3.folder.Folder; +import com.android.launcher3.folder.FolderIcon; +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.notification.NotificationListener; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.popup.PopupLiveUpdateHandler; import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.shortcuts.DeepShortcutView; +import com.android.launcher3.splitscreen.SplitShortcut; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.LauncherBindableItemsContainer; +import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.util.ShortcutUtil; +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 java.io.PrintWriter; import java.util.HashMap; +import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Implements interfaces required to show and allow interacting with a PopupContainerWithArrow. + * Controls the long-press menu on Taskbar and AllApps icons. */ -public class TaskbarPopupController { +public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController { - private static final SystemShortcut.Factory + private static final SystemShortcut.Factory APP_INFO = SystemShortcut.AppInfo::new; + private final TaskbarActivityContext mContext; private final PopupDataProvider mPopupDataProvider; - public TaskbarPopupController() { - // TODO (b/198438631): add notifications dots change listener - mPopupDataProvider = new PopupDataProvider(packageUserKey -> {}); + // Initialized in init. + private TaskbarControllers mControllers; + private boolean mAllowInitialSplitSelection; + + public TaskbarPopupController(TaskbarActivityContext context) { + mContext = context; + mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots); + } + + public void init(TaskbarControllers controllers) { + mControllers = controllers; + + NotificationListener.addNotificationsChangedListener(mPopupDataProvider); + } + + public void onDestroy() { + NotificationListener.removeNotificationsChangedListener(mPopupDataProvider); } @NonNull @@ -55,36 +103,236 @@ public class TaskbarPopupController { mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy); } + public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) { + mAllowInitialSplitSelection = allowInitialSplitSelection; + } + + private void updateNotificationDots(Predicate updatedDots) { + final PackageUserKey packageUserKey = new PackageUserKey(null, null); + Predicate matcher = info -> !packageUserKey.updateFromItemInfo(info) + || updatedDots.test(packageUserKey); + + LauncherBindableItemsContainer.ItemOperator op = (info, v) -> { + if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) { + if (matcher.test(info)) { + ((BubbleTextView) v).applyDotState(info, true /* animate */); + } + } else if (info instanceof FolderInfo && v instanceof FolderIcon) { + FolderInfo fi = (FolderInfo) info; + if (fi.contents.stream().anyMatch(matcher)) { + FolderDotInfo folderDotInfo = new FolderDotInfo(); + for (WorkspaceItemInfo si : fi.contents) { + folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si)); + } + ((FolderIcon) v).setDotInfo(folderDotInfo); + } + } + + // process all the shortcuts + return false; + }; + + mControllers.taskbarViewController.mapOverItems(op); + Folder folder = Folder.getOpen(mContext); + if (folder != null) { + folder.iterateOverItems(op); + } + } + /** * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}. * @return the container if shown or null. */ - public PopupContainerWithArrow showForIcon(BubbleTextView icon) { - TaskbarActivityContext context = ActivityContext.lookupContext(icon.getContext()); + public PopupContainerWithArrow showForIcon(BubbleTextView icon) { + BaseTaskbarContext context = ActivityContext.lookupContext(icon.getContext()); if (PopupContainerWithArrow.getOpen(context) != null) { // There is already an items container open, so don't open this one. icon.clearFocus(); return null; } ItemInfo item = (ItemInfo) icon.getTag(); - if (!PopupContainerWithArrow.canShow(icon, item)) { + if (!ShortcutUtil.supportsShortcuts(item)) { return null; } - final PopupContainerWithArrow container = - (PopupContainerWithArrow) context.getLayoutInflater().inflate( - R.layout.popup_container, context.getDragLayer(), false); - // TODO (b/198438631): configure for taskbar/context + PopupContainerWithArrow container; + int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(item); + // TODO(b/198438631): add support for INSTALL shortcut factory + List systemShortcuts = getSystemShortcuts() + .map(s -> s.getShortcut(context, item, icon)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - container.populateAndShow(icon, - mPopupDataProvider.getShortcutCountForItem(item), - mPopupDataProvider.getNotificationKeysForItem(item), - // TODO (b/198438631): add support for INSTALL shortcut factory - Stream.of(APP_INFO) - .map(s -> s.getShortcut(context, item)) - .filter(Objects::nonNull) - .collect(Collectors.toList())); + if (ENABLE_MATERIAL_U_POPUP.get()) { + container = (PopupContainerWithArrow) context.getLayoutInflater().inflate( + R.layout.popup_container_material_u, context.getDragLayer(), false); + container.populateAndShowRowsMaterialU(icon, deepShortcutCount, systemShortcuts); + } else { + container = (PopupContainerWithArrow) context.getLayoutInflater().inflate( + R.layout.popup_container, context.getDragLayer(), false); + container.populateAndShow( + icon, + deepShortcutCount, + mPopupDataProvider.getNotificationKeysForItem(item), + systemShortcuts); + icon.clearAccessibilityFocus(); + } + + container.addOnAttachStateChangeListener( + new PopupLiveUpdateHandler(context, container) { + @Override + protected void showPopupContainerForIcon(BubbleTextView originalIcon) { + showForIcon(originalIcon); + } + }); + // TODO (b/198438631): configure for taskbar/context + container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler()); + mControllers.taskbarDragController.addDragListener(container); container.requestFocus(); + + // Make focusable to receive back events + context.onPopupVisibilityChanged(true); + container.addOnCloseCallback(() -> { + context.getDragLayer().post(() -> context.onPopupVisibilityChanged(false)); + }); + return container; } + + // 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), + Utilities.getSplitPositionOptions(mContext.getDeviceProfile()) + .stream() + .map(this::createSplitShortcutFactory) + ); + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarPopupController:"); + + mPopupDataProvider.dump(prefix + "\t", pw); + } + + private class TaskbarPopupItemDragHandler implements + PopupContainerWithArrow.PopupItemDragHandler { + + protected final Point mIconLastTouchPos = new Point(); + + TaskbarPopupItemDragHandler() {} + + @Override + public boolean onTouch(View view, MotionEvent ev) { + // Touched a shortcut, update where it was touched so we can drag from there on + // long click. + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if not the correct view + if (!(v.getParent() instanceof DeepShortcutView)) return false; + + DeepShortcutView sv = (DeepShortcutView) v.getParent(); + sv.setWillDrawIcon(false); + + // Move the icon to align with the center-top of the touch point + Point iconShift = new Point(); + iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; + iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize; + + ((TaskbarDragController) ActivityContext.lookupContext( + v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift); + + return false; + } + } + + /** + * Creates a factory function representing a single "split position" menu item ("Split left," + * "Split right," or "Split top"). + * @param position A SplitPositionOption representing whether we are splitting top, left, or + * right. + * @return A factory function to be used in populating the long-press menu. + */ + private SystemShortcut.Factory createSplitShortcutFactory( + SplitPositionOption position) { + return (context, itemInfo, originalView) -> new TaskbarSplitShortcut(context, itemInfo, + originalView, position, mAllowInitialSplitSelection); + } + + /** + * 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. + */ + private static class TaskbarSplitShortcut extends + SplitShortcut { + /** + * If {@code true}, clicking this shortcut will not attempt to start a split app directly, + * but be the first app in split selection mode + */ + private final boolean mAllowInitialSplitSelection; + + TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView, + SplitPositionOption position, boolean allowInitialSplitSelection) { + super(position.iconResId, position.textResId, context, itemInfo, originalView, + position); + mAllowInitialSplitSelection = allowInitialSplitSelection; + } + + @Override + public void onClick(View view) { + // Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps) + mTarget.onSplitScreenMenuButtonClicked(); + AbstractFloatingView.closeAllOpenViews(mTarget); + + // Depending on what app state we're in, we either want to initiate the split screen + // staging process or immediately launch a split with an existing app. + // - Initiate the split screen staging process + if (mAllowInitialSplitSelection) { + super.onClick(view); + return; + } + + // - Immediately launch split with the running app + Pair instanceIds = + LogUtils.getShellShareableInstanceId(); + mTarget.getStatsLogManager().logger() + .withItemInfo(mItemInfo) + .withInstanceId(instanceIds.second) + .log(getLogEventForPosition(getPosition().stagePosition)); + + if (mItemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo; + SystemUiProxy.INSTANCE.get(mTarget).startShortcut( + workspaceItemInfo.getIntent().getPackage(), + workspaceItemInfo.getDeepShortcutId(), + getPosition().stagePosition, + null, + workspaceItemInfo.user, + instanceIds.first); + } else { + SystemUiProxy.INSTANCE.get(mTarget).startIntent( + mTarget.getSystemService(LauncherApps.class).getMainActivityLaunchIntent( + mItemInfo.getIntent().getComponent(), + null, + mItemInfo.user), + new Intent(), + getPosition().stagePosition, + null, + instanceIds.first); + } + } + } } + diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java new file mode 100644 index 0000000000..8445cff0ee --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java @@ -0,0 +1,63 @@ +/* + * 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; + +import android.util.SparseArray; + +import androidx.annotation.CallSuper; + +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; + +/** + * Base class for providing recent apps functionality + */ +public class TaskbarRecentAppsController { + + public static final TaskbarRecentAppsController DEFAULT = new TaskbarRecentAppsController(); + + // Initialized in init. + protected TaskbarControllers mControllers; + + @CallSuper + protected void init(TaskbarControllers taskbarControllers) { + mControllers = taskbarControllers; + } + + @CallSuper + protected void onDestroy() { + mControllers = null; + } + + /** Stores the current {@link AppInfo} instances, no-op except in desktop environment. */ + protected void setApps(AppInfo[] apps) { } + + /** + * Indicates whether recent apps functionality is enabled, should return false except in + * desktop environment. + */ + protected boolean isEnabled() { + return false; + } + + /** Called to update hotseatItems, no-op except in desktop environment. */ + protected ItemInfo[] updateHotseatItemInfos(ItemInfo[] hotseatItems) { + return hotseatItems; + } + + /** Called to update the list of currently running apps, no-op except in desktop environment. */ + protected void updateRunningApps(SparseArray hotseatItems) { } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java index 94a3307bc3..cdc6d59bf1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java @@ -17,22 +17,19 @@ package com.android.launcher3.taskbar; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; import android.util.AttributeSet; import android.view.View; +import com.android.launcher3.views.ActivityContext; + /** * View that handles scrimming the taskbar and the inverted corners it draws. The scrim is used * when bubbles is expanded. */ public class TaskbarScrimView extends View { - private final Paint mTaskbarScrimPaint; - private final Path mInvertedLeftCornerPath, mInvertedRightCornerPath; + private final TaskbarBackgroundRenderer mRenderer; private boolean mShowScrim; - private float mLeftCornerRadius, mRightCornerRadius; - private float mBackgroundHeight; public TaskbarScrimView(Context context) { this(context, null); @@ -49,14 +46,9 @@ public class TaskbarScrimView extends View { public TaskbarScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - - mTaskbarScrimPaint = new Paint(); - mTaskbarScrimPaint.setColor(getResources().getColor(android.R.color.system_neutral1_1000)); - mTaskbarScrimPaint.setFlags(Paint.ANTI_ALIAS_FLAG); - mTaskbarScrimPaint.setStyle(Paint.Style.FILL); - - mInvertedLeftCornerPath = new Path(); - mInvertedRightCornerPath = new Path(); + mRenderer = new TaskbarBackgroundRenderer(ActivityContext.lookupContext(context)); + mRenderer.getPaint().setColor(getResources().getColor( + android.R.color.system_neutral1_1000)); } @Override @@ -64,31 +56,7 @@ public class TaskbarScrimView extends View { super.onDraw(canvas); if (mShowScrim) { - canvas.save(); - canvas.translate(0, canvas.getHeight() - mBackgroundHeight); - - // Scrim the taskbar itself. - canvas.drawRect(0, 0, canvas.getWidth(), mBackgroundHeight, mTaskbarScrimPaint); - - // Scrim the inverted rounded corners above the taskbar. - canvas.translate(0, -mLeftCornerRadius); - canvas.drawPath(mInvertedLeftCornerPath, mTaskbarScrimPaint); - canvas.translate(0, mLeftCornerRadius); - canvas.translate(canvas.getWidth() - mRightCornerRadius, -mRightCornerRadius); - canvas.drawPath(mInvertedRightCornerPath, mTaskbarScrimPaint); - - canvas.restore(); - } - } - - /** - * Sets the height of the taskbar background. - * @param height the height of the background. - */ - protected void setBackgroundHeight(float height) { - mBackgroundHeight = height; - if (mShowScrim) { - invalidate(); + mRenderer.draw(canvas); } } @@ -98,32 +66,16 @@ public class TaskbarScrimView extends View { */ protected void setScrimAlpha(float alpha) { mShowScrim = alpha > 0f; - mTaskbarScrimPaint.setAlpha((int) (alpha * 255)); + mRenderer.getPaint().setAlpha((int) (alpha * 255)); invalidate(); } /** - * Sets the radius of the left and right corners above the taskbar. - * @param leftCornerRadius the radius of the left corner. - * @param rightCornerRadius the radius of the right corner. + * Sets the roundness of the round corner above Taskbar. + * @param cornerRoundness 0 has no round corner, 1 has complete round corner. */ - protected void setCornerSizes(float leftCornerRadius, float rightCornerRadius) { - mLeftCornerRadius = leftCornerRadius; - mRightCornerRadius = rightCornerRadius; - - Path square = new Path(); - square.addRect(0, 0, mLeftCornerRadius, mLeftCornerRadius, Path.Direction.CW); - Path circle = new Path(); - circle.addCircle(mLeftCornerRadius, 0, mLeftCornerRadius, Path.Direction.CW); - mInvertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE); - square.reset(); - square.addRect(0, 0, mRightCornerRadius, mRightCornerRadius, Path.Direction.CW); - circle.reset(); - circle.addCircle(0, 0, mRightCornerRadius, Path.Direction.CW); - mInvertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE); - - if (mShowScrim) { - invalidate(); - } + protected void setCornerRoundness(float cornerRoundness) { + mRenderer.setCornerRoundness(cornerRoundness); + invalidate(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java index 4b4ee4423c..5ea00cf408 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java @@ -22,13 +22,16 @@ import android.animation.ObjectAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; -import com.android.quickstep.AnimatedFloat; +import com.android.launcher3.anim.AnimatedFloat; import com.android.quickstep.SystemUiProxy; +import java.io.PrintWriter; + /** * Handles properties/data collection, and passes the results to {@link TaskbarScrimView} to render. */ -public class TaskbarScrimViewController { +public class TaskbarScrimViewController implements TaskbarControllers.LoggableTaskbarController, + TaskbarControllers.BackgroundRendererController { private static final float SCRIM_ALPHA = 0.6f; @@ -47,9 +50,6 @@ public class TaskbarScrimViewController { public TaskbarScrimViewController(TaskbarActivityContext activity, TaskbarScrimView scrimView) { mActivity = activity; mScrimView = scrimView; - mScrimView.setCornerSizes(mActivity.getLeftCornerRadius(), - mActivity.getRightCornerRadius()); - mScrimView.setBackgroundHeight(mActivity.getDeviceProfile().taskbarSize); } /** @@ -67,7 +67,8 @@ public class TaskbarScrimViewController { final boolean manageMenuExpanded = (stateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0; final boolean showScrim = !mControllers.navbarButtonsViewController.isImeVisible() - && bubblesExpanded && mControllers.taskbarStashController.isInAppAndNotStashed(); + && bubblesExpanded + && mControllers.taskbarStashController.isTaskbarVisibleAndNotStashing(); final float scrimAlpha = manageMenuExpanded // When manage menu shows there's the first scrim and second scrim so figure out // what the total transparency would be. @@ -94,4 +95,16 @@ public class TaskbarScrimViewController { private void onClick() { SystemUiProxy.INSTANCE.get(mActivity).onBackPressed(); } + + @Override + public void setCornerRoundness(float cornerRoundness) { + mScrimView.setCornerRoundness(cornerRoundness); + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarScrimViewController:"); + + pw.println(prefix + "\tmScrimAlpha.value=" + mScrimAlpha.value); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java index 23beef0a8a..66ca7d927f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java @@ -15,13 +15,37 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.taskbar.LauncherTaskbarUIController.DISPLAY_PROGRESS_COUNT; + +import android.app.PendingIntent; + /** * State shared across different taskbar instance */ public class TaskbarSharedState { + // TaskbarManager#onSystemUiFlagsChanged public int sysuiStateFlags; + // TaskbarManager#disableNavBarElements() + public int disableNavBarDisplayId; + public int disableNavBarState1; + public int disableNavBarState2; + + // TaskbarManager#onSystemBarAttributesChanged() + public int systemBarAttrsDisplayId; + public int systemBarAttrsBehavior; + + // TaskbarManager#onNavButtonsDarkIntensityChanged() + public float navButtonsDarkIntensity; + public boolean setupUIVisible = false; + public boolean allAppsVisible = false; + + // LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp + public float[] inAppDisplayProgressMultiPropValues = new float[DISPLAY_PROGRESS_COUNT]; + + // Taskbar System Action + public PendingIntent taskbarSystemActionPendingIntent; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java new file mode 100644 index 0000000000..c10b57ab87 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DEEP_SHORTCUTS; +import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.SHORTCUTS_AND_NOTIFICATIONS; +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.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; + +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.View; + +import com.android.internal.logging.InstanceId; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.R; +import com.android.launcher3.accessibility.BaseAccessibilityDelegate; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.notification.NotificationListener; +import com.android.launcher3.util.ShortcutUtil; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.util.LogUtils; + +import java.util.List; + +/** + * Accessibility delegate for the Taskbar. This provides an accessible interface for taskbar + * features. + */ +public class TaskbarShortcutMenuAccessibilityDelegate + extends BaseAccessibilityDelegate { + + public static final int MOVE_TO_TOP_OR_LEFT = R.id.action_move_to_top_or_left; + public static final int MOVE_TO_BOTTOM_OR_RIGHT = R.id.action_move_to_bottom_or_right; + + private final LauncherApps mLauncherApps; + private final StatsLogManager mStatsLogManager; + + public TaskbarShortcutMenuAccessibilityDelegate(TaskbarActivityContext context) { + super(context); + mLauncherApps = context.getSystemService(LauncherApps.class); + mStatsLogManager = context.getStatsLogManager(); + + mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS, + R.string.action_deep_shortcut, KeyEvent.KEYCODE_S)); + mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS, + R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S)); + mActions.put(MOVE_TO_TOP_OR_LEFT, new LauncherAction( + MOVE_TO_TOP_OR_LEFT, R.string.move_drop_target_top_or_left, KeyEvent.KEYCODE_L)); + mActions.put(MOVE_TO_BOTTOM_OR_RIGHT, new LauncherAction( + MOVE_TO_BOTTOM_OR_RIGHT, + R.string.move_drop_target_bottom_or_right, + KeyEvent.KEYCODE_R)); + } + + @Override + protected void getSupportedActions(View host, ItemInfo item, List out) { + if (ShortcutUtil.supportsShortcuts(item) && FeatureFlags.ENABLE_TASKBAR_POPUP_MENU.get()) { + out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null + ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS)); + } + out.add(mActions.get(MOVE_TO_TOP_OR_LEFT)); + out.add(mActions.get(MOVE_TO_BOTTOM_OR_RIGHT)); + } + + @Override + protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { + if (item instanceof WorkspaceItemInfo + && (action == MOVE_TO_TOP_OR_LEFT || action == MOVE_TO_BOTTOM_OR_RIGHT)) { + WorkspaceItemInfo info = (WorkspaceItemInfo) item; + int side = action == MOVE_TO_TOP_OR_LEFT + ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; + + Pair instanceIds = + LogUtils.getShellShareableInstanceId(); + mStatsLogManager.logger() + .withItemInfo(item) + .withInstanceId(instanceIds.second) + .log(getLogEventForPosition(side)); + + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + SystemUiProxy.INSTANCE.get(mContext).startShortcut( + info.getIntent().getPackage(), + info.getDeepShortcutId(), + side, + /* bundleOpts= */ null, + info.user, + instanceIds.first); + } else { + SystemUiProxy.INSTANCE.get(mContext).startIntent( + mLauncherApps.getMainActivityLaunchIntent( + item.getIntent().getComponent(), + /* startActivityOptions= */null, + item.user), + new Intent(), side, null, instanceIds.first); + } + return true; + } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) { + mContext.showPopupMenuForIcon((BubbleTextView) host); + + return true; + } + return false; + } + + @Override + protected boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) { + return false; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java new file mode 100644 index 0000000000..d65b5c0f61 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static com.android.launcher3.anim.AnimatedFloat.VALUE; + +import android.animation.ValueAnimator; + +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController; +import com.android.launcher3.util.DisplayController; + +import java.io.PrintWriter; + +/** + * Manages the spring animation when stashing the transient taskbar. + */ +public class TaskbarSpringOnStashController implements LoggableTaskbarController { + + private final TaskbarActivityContext mContext; + private TaskbarControllers mControllers; + private final AnimatedFloat mTranslationForStash = new AnimatedFloat( + this::updateTranslationYForStash); + + private final boolean mIsTransientTaskbar; + + private final float mStartVelocityPxPerS; + + public TaskbarSpringOnStashController(TaskbarActivityContext context) { + mContext = context; + mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext); + mStartVelocityPxPerS = context.getResources() + .getDimension(R.dimen.transient_taskbar_stash_spring_velocity_dp_per_s); + } + + /** + * Initialization method. + */ + public void init(TaskbarControllers controllers) { + mControllers = controllers; + } + + private void updateTranslationYForStash() { + if (!mIsTransientTaskbar) { + return; + } + + float transY = mTranslationForStash.value; + mControllers.stashedHandleViewController.setTranslationYForStash(transY); + mControllers.taskbarViewController.setTranslationYForStash(transY); + mControllers.taskbarDragLayerController.setTranslationYForStash(transY); + } + + /** + * Returns a spring animation to be used when stashing the transient taskbar. + */ + public @Nullable ValueAnimator createSpringToStash() { + if (!mIsTransientTaskbar) { + return null; + } + return new SpringAnimationBuilder(mContext) + .setStartValue(mTranslationForStash.value) + .setEndValue(0) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .setStartVelocity(mStartVelocityPxPerS) + .build(mTranslationForStash, VALUE); + } + + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarSpringOnStashController:"); + + pw.println(prefix + "\tmTranslationForStash=" + mTranslationForStash.value); + pw.println(prefix + "\tmStartVelocityPxPerS=" + mStartVelocityPxPerS); + } +} + diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 7f7585000c..69ea9fd03a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -16,58 +16,124 @@ package com.android.launcher3.taskbar; import static android.view.HapticFeedbackConstants.LONG_PRESS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; +import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; +import static com.android.launcher3.anim.Interpolators.INSTANT; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.config.FeatureFlags.FORCE_PERSISTENT_TASKBAR; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW; +import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED; +import static com.android.launcher3.taskbar.TaskbarManager.SYSTEM_ACTION_ID_TASKBAR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +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_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; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.annotation.Nullable; +import android.app.RemoteAction; import android.content.SharedPreferences; -import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.SystemClock; +import android.util.Log; +import android.view.InsetsController; +import android.view.View; import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Interpolator; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.jank.InteractionJankMonitor; +import com.android.launcher3.Alarm; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; -import com.android.quickstep.AnimatedFloat; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.quickstep.SystemUiProxy; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.StringJoiner; import java.util.function.IntPredicate; /** * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to * create a cohesive animation between stashed/unstashed states. */ -public class TaskbarStashController { +public class TaskbarStashController implements TaskbarControllers.LoggableTaskbarController { + private static final String TAG = TaskbarStashController.class.getSimpleName(); + private static final boolean DEBUG = false; public static final int FLAG_IN_APP = 1 << 0; public static final int FLAG_STASHED_IN_APP_MANUAL = 1 << 1; // long press, persisted - public static final int FLAG_STASHED_IN_APP_PINNED = 1 << 2; // app pinning - public static final int FLAG_STASHED_IN_APP_EMPTY = 1 << 3; // no hotseat icons - public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 4; // setup wizard and AllSetActivity - public static final int FLAG_STASHED_IN_APP_IME = 1 << 5; // IME is visible - public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 6; + public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 2; // shade open, ... + public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 3; // setup wizard and AllSetActivity + public static final int FLAG_STASHED_IN_APP_IME = 1 << 4; // IME is visible + public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 5; + public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 6; // All apps is visible. + public static final int FLAG_IN_SETUP = 1 << 7; // In the Setup Wizard + public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 8; // phone screen gesture nav, stashed + public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 9; // Autohide (transient taskbar). + public static final int FLAG_STASHED_SYSUI = 1 << 10; // app pinning,... + public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 11; // device is locked: keyguard, ... + + // If any of these flags are enabled, isInApp should return true. + private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP; // If we're in an app and any of these flags are enabled, taskbar should be stashed. private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL - | FLAG_STASHED_IN_APP_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP - | FLAG_STASHED_IN_APP_IME; + | FLAG_STASHED_IN_APP_SYSUI | FLAG_STASHED_IN_APP_SETUP + | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS + | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO; + + private static final int FLAGS_STASHED_IN_APP_IGNORING_IME = + FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME; // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed // height. This way the reported insets are consistent even during transitions out of the app. - // Currently any flag that causes us to stash in an app is included, except for IME since that - // covers the underlying app anyway and thus the app shouldn't change insets. + // Currently any flag that causes us to stash in an app is included, except for IME or All Apps + // since those cover the underlying app anyway and thus the app shouldn't change insets. private static final int FLAGS_REPORT_STASHED_INSETS_TO_APP = FLAGS_STASHED_IN_APP - & ~FLAG_STASHED_IN_APP_IME; + & ~FLAG_STASHED_IN_APP_IME & ~FLAG_STASHED_IN_TASKBAR_ALL_APPS; + + // 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; /** * How long to stash/unstash when manually invoked via long press. + * + * Use {@link #getStashDuration()} to query duration */ - public static final long TASKBAR_STASH_DURATION = 300; + private static final long TASKBAR_STASH_DURATION = + InsetsController.ANIMATION_DURATION_RESIZE; + + /** + * How long to stash/unstash transient taskbar. + * + * Use {@link #getStashDuration()} to query duration. + */ + private static final long TRANSIENT_TASKBAR_STASH_DURATION = 417; /** * How long to stash/unstash when keyboard is appearing/disappearing. @@ -77,7 +143,7 @@ public class TaskbarStashController { /** * The scale TaskbarView animates to when being stashed. */ - private static final float STASHED_TASKBAR_SCALE = 0.5f; + protected static final float STASHED_TASKBAR_SCALE = 0.5f; /** * How long the hint animation plays, starting on motion down. @@ -85,6 +151,21 @@ public class TaskbarStashController { private static final long TASKBAR_HINT_STASH_DURATION = ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT; + /** + * How long to delay the icon/stash handle alpha. + */ + private 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; + + /** + * How long to delay the icon/stash handle alpha for the home to app taskbar animation. + */ + private static final long TASKBAR_STASH_ICON_ALPHA_HOME_TO_APP_START_DELAY = 66; + /** * The scale that TaskbarView animates to when hinting towards the stashed state. */ @@ -105,6 +186,43 @@ public class TaskbarStashController { */ private static final boolean DEFAULT_STASHED_PREF = false; + // Auto stashes when user has not interacted with the Taskbar after X ms. + private static final long NO_TOUCH_TIMEOUT_TO_STASH_MS = 5000; + + // Duration for which an unlock event is considered "current", as other events are received + // asynchronously. + private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200; + + /** + * The default stash animation, morphing the taskbar into the navbar. + */ + private static final int TRANSITION_DEFAULT = 0; + /** + * Transitioning from launcher to app. Same as TRANSITION_DEFAULT, differs in internal + * animation timings. + */ + private static final int TRANSITION_HOME_TO_APP = 1; + /** + * Fading the navbar in and out, where the taskbar jumpcuts in and out at the very begin/end of + * the transition. Used to transition between the hotseat and navbar` without the stash/unstash + * transition. + */ + private static final int TRANSITION_HANDLE_FADE = 2; + /** + * Same as TRANSITION_DEFAULT, but exclusively used during an "navbar unstash to hotseat + * animation" bound to the progress of a swipe gesture. It differs from TRANSITION_DEFAULT + * by not scaling the height of the taskbar background. + */ + private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3; + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TRANSITION_DEFAULT, + TRANSITION_HOME_TO_APP, + TRANSITION_HANDLE_FADE, + TRANSITION_UNSTASH_SUW_MANUAL, + }) + private @interface StashAnimation {} + private final TaskbarActivityContext mActivity; private final SharedPreferences mPrefs; private final int mStashedHeight; @@ -117,12 +235,13 @@ public class TaskbarStashController { private AnimatedFloat mTaskbarBackgroundOffset; private AnimatedFloat mTaskbarImeBgAlpha; // TaskbarView icon properties. - private AlphaProperty mIconAlphaForStash; + private MultiProperty mIconAlphaForStash; private AnimatedFloat mIconScaleForStash; private AnimatedFloat mIconTranslationYForStash; // Stashed handle properties. - private AlphaProperty mTaskbarStashedHandleAlpha; + private MultiProperty mTaskbarStashedHandleAlpha; private AnimatedFloat mTaskbarStashedHandleHintScale; + private final AccessibilityManager mAccessibilityManager; /** Whether we are currently visually stashed (might change based on launcher state). */ private boolean mIsStashed = false; @@ -131,55 +250,93 @@ public class TaskbarStashController { private @Nullable AnimatorSet mAnimator; private boolean mIsSystemGestureInProgress; private boolean mIsImeShowing; + private boolean mIsImeSwitcherShowing; + + private boolean mEnableManualStashingDuringTests = false; + + private final Alarm mTimeoutAlarm = new Alarm(); + private boolean mEnableBlockingTimeoutDuringTests = false; // Evaluate whether the handle should be stashed private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder( flags -> { - boolean inApp = hasAnyFlag(flags, FLAG_IN_APP); + boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP); boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP); boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE); - return (inApp && stashedInApp) || (!inApp && stashedLauncherState); + boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED); + return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed; }); + private boolean mIsTaskbarSystemActionRegistered = false; + private TaskbarSharedState mTaskbarSharedState; + public TaskbarStashController(TaskbarActivityContext activity) { mActivity = activity; - mPrefs = Utilities.getPrefs(mActivity); - final Resources resources = mActivity.getResources(); - mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); + mPrefs = LauncherPrefs.getPrefs(mActivity); mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); - mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize; + mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class); + + mUnstashedHeight = mActivity.getDeviceProfile().taskbarHeight; + mStashedHeight = mActivity.getDeviceProfile().stashedTaskbarHeight; } - public void init(TaskbarControllers controllers, TaskbarSharedState sharedState) { + /** + * 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 + */ + public void init( + TaskbarControllers controllers, + boolean setupUIVisible, + TaskbarSharedState sharedState) { mControllers = controllers; + mTaskbarSharedState = sharedState; TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController; mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset(); mTaskbarImeBgAlpha = dragLayerController.getImeBgTaskbar(); TaskbarViewController taskbarViewController = controllers.taskbarViewController; - mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty( + mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().get( TaskbarViewController.ALPHA_INDEX_STASH); mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash(); mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash(); StashedHandleViewController stashedHandleController = controllers.stashedHandleViewController; - mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha().getProperty( + mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha().get( StashedHandleViewController.ALPHA_INDEX_STASHED); mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale(); - boolean isManuallyStashedInApp = supportsManualStashing() + boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); + // We use supportsVisualStashing() here instead of supportsManualStashing() because we want + // it to work properly for tests that recreate taskbar. This check is here just to ensure + // that taskbar unstashes when going to 3 button mode (supportsVisualStashing() false). + boolean isManuallyStashedInApp = supportsVisualStashing() + && !isTransientTaskbar + && !FORCE_PERSISTENT_TASKBAR.get() && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF); - boolean isInSetup = !mActivity.isUserSetupComplete() || sharedState.setupUIVisible; + boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible; updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp); - // TODO(b/204384193): Temporarily disable SUW specific logic - // updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup); - if (isInSetup) { - // Update the in-app state to ensure isStashed() reflects right state during SUW - updateStateForFlag(FLAG_IN_APP, true); - } - applyState(); + updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isTransientTaskbar); + updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup); + updateStateForFlag(FLAG_IN_SETUP, isInSetup); + updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, isPhoneMode() + && !mActivity.isThreeButtonNav()); + // 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); notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp()); } @@ -188,30 +345,58 @@ public class TaskbarStashController { * Returns whether the taskbar can visually stash into a handle based on the current device * state. */ - private boolean supportsVisualStashing() { - return !mActivity.isThreeButtonNav(); + public boolean supportsVisualStashing() { + return !mActivity.isThreeButtonNav() && mControllers.uiController.supportsVisualStashing(); } /** * Returns whether the user can manually stash the taskbar based on the current device state. */ - private boolean supportsManualStashing() { + protected boolean supportsManualStashing() { + if (FORCE_PERSISTENT_TASKBAR.get()) { + return false; + } return supportsVisualStashing() - && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || supportsStashingForTests()); + && isInApp() + && (!Utilities.isRunningInTestHarness() || mEnableManualStashingDuringTests) + && !DisplayController.isTransientTaskbar(mActivity); } - private boolean supportsStashingForTests() { - // TODO: enable this for tests that specifically check stash/unstash behavior. - return false; + /** + * Enables support for manual stashing. This should only be used to add this functionality + * to Launcher specific tests. + */ + @VisibleForTesting + public void enableManualStashingDuringTests(boolean enableManualStashing) { + mEnableManualStashingDuringTests = enableManualStashing; + } + + /** + * Enables the auto timeout for taskbar stashing. This method should only be used for taskbar + * testing. + */ + @VisibleForTesting + public void enableBlockingTimeoutDuringTests(boolean enableBlockingTimeout) { + mEnableBlockingTimeoutDuringTests = enableBlockingTimeout; } /** * Sets the flag indicating setup UI is visible */ protected void setSetupUIVisible(boolean isVisible) { - updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, - isVisible || !mActivity.isUserSetupComplete()); - applyState(); + boolean hideTaskbar = isVisible || !mActivity.isUserSetupComplete(); + updateStateForFlag(FLAG_IN_SETUP, hideTaskbar); + updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, hideTaskbar); + applyState(hideTaskbar ? 0 : getStashDuration()); + } + + /** + * Returns how long the stash/unstash animation should play. + */ + public long getStashDuration() { + return DisplayController.isTransientTaskbar(mActivity) + ? TRANSIENT_TASKBAR_STASH_DURATION + : TASKBAR_STASH_DURATION; } /** @@ -228,11 +413,25 @@ public class TaskbarStashController { return hasAnyFlag(FLAGS_STASHED_IN_APP); } + /** + * Returns whether the taskbar should be stashed in apps regardless of the IME visibility. + */ + public boolean isStashedInAppIgnoringIme() { + return hasAnyFlag(FLAGS_STASHED_IN_APP_IGNORING_IME); + } + /** * Returns whether the taskbar should be stashed in the current LauncherState. */ public boolean isInStashedLauncherState() { - return hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing(); + return (hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing()); + } + + /** + * @return {@code true} if we're not on a large screen AND using gesture nav + */ + private boolean isPhoneMode() { + return TaskbarManager.isPhoneMode(mActivity.getDeviceProfile()); } private boolean hasAnyFlag(int flagMask) { @@ -245,28 +444,100 @@ public class TaskbarStashController { /** - * Returns whether the taskbar is currently visible and in an app. + * Returns whether the taskbar is currently visible and not in the process of being stashed. */ - public boolean isInAppAndNotStashed() { - return !mIsStashed && (mState & FLAG_IN_APP) != 0; + public boolean isTaskbarVisibleAndNotStashing() { + return !mIsStashed && mControllers.taskbarViewController.areIconsVisible(); + } + + public boolean isInApp() { + return hasAnyFlag(FLAGS_IN_APP); + } + + /** + * Returns the height that taskbar will be touchable. + */ + public int getTouchableHeight() { + return mIsStashed + ? mStashedHeight + : (mUnstashedHeight + mActivity.getDeviceProfile().taskbarBottomMargin); } /** * Returns the height that taskbar will inset when inside apps. + * @see android.view.WindowInsets.Type#navigationBars() + * @see android.view.WindowInsets.Type#systemBars() */ public int getContentHeightToReportToApps() { - if (hasAnyFlag(FLAGS_REPORT_STASHED_INSETS_TO_APP)) { - boolean isAnimating = mAnimator != null && mAnimator.isStarted(); - return mControllers.stashedHandleViewController.isStashedHandleVisible() || isAnimating - ? mStashedHeight : 0; + if ((isPhoneMode() && !mActivity.isThreeButtonNav()) + || DisplayController.isTransientTaskbar(mActivity)) { + return getStashedHeight(); } + + if (supportsVisualStashing() && hasAnyFlag(FLAGS_REPORT_STASHED_INSETS_TO_APP)) { + DeviceProfile dp = mActivity.getDeviceProfile(); + if (hasAnyFlag(FLAG_STASHED_IN_APP_SETUP) && dp.isTaskbarPresent) { + // We always show the back button in SUW but in portrait the SUW layout may not + // be wide enough to support overlapping the nav bar with its content. + // We're sending different res values in portrait vs landscape + return mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_suw_insets); + } + boolean isAnimating = mAnimator != null && mAnimator.isStarted(); + if (!mControllers.stashedHandleViewController.isStashedHandleVisible() + && isInApp() + && !isAnimating) { + // We are in a settled state where we're not showing the handle even though taskbar + // is stashed. This can happen for example when home button is disabled (see + // StashedHandleViewController#setIsHomeButtonDisabled()). + return 0; + } + return mStashedHeight; + } + return mUnstashedHeight; } + /** + * Returns the height that taskbar will inset when inside apps. + * @see android.view.WindowInsets.Type#tappableElement() + */ + public int getTappableHeightToReportToApps() { + int contentHeight = getContentHeightToReportToApps(); + return contentHeight <= mStashedHeight ? 0 : contentHeight; + } + public int getStashedHeight() { return mStashedHeight; } + /** + * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION. + */ + public void updateAndAnimateTransientTaskbar(boolean stash) { + updateAndAnimateTransientTaskbar(stash, TASKBAR_STASH_DURATION); + } + + /** + * Stash or unstashes the transient taskbar. + */ + public void updateAndAnimateTransientTaskbar(boolean stash, long duration) { + if (!DisplayController.isTransientTaskbar(mActivity)) { + return; + } + + if (stash && mControllers.taskbarAutohideSuspendController.isSuspended() + && !mControllers.taskbarAutohideSuspendController + .isSuspendedForTransientTaskbarInOverview()) { + // Avoid stashing if autohide is currently suspended. + return; + } + + if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) { + updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash); + applyState(); + } + } + /** * Should be called when long pressing the nav region when taskbar is present. * @return Whether taskbar was stashed and now is unstashed. @@ -277,6 +548,9 @@ public class TaskbarStashController { // taskbar, we use an OnLongClickListener on TaskbarView instead. return false; } + if (!canCurrentlyManuallyUnstash()) { + return false; + } if (updateAndAnimateIsManuallyStashedInApp(false)) { mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS); return true; @@ -284,6 +558,16 @@ public class TaskbarStashController { return false; } + /** + * Returns whether taskbar will unstash when long pressing it based on the current state. The + * only time this is true is if the user is in an app and the taskbar is only stashed because + * the user previously long pressed to manually stash (not due to other reasons like IME). + */ + private boolean canCurrentlyManuallyUnstash() { + return (mState & (FLAG_IN_APP | FLAGS_STASHED_IN_APP)) + == (FLAG_IN_APP | FLAG_STASHED_IN_APP_MANUAL); + } + /** * Updates whether we should stash the taskbar when in apps, and animates to the changed state. * @return Whether we started an animation to either be newly stashed or unstashed. @@ -301,34 +585,95 @@ public class TaskbarStashController { return false; } + /** + * Adds the Taskbar unstash to Hotseat animator to the animator set. + * + * This should be used to run a Taskbar unstash to Hotseat animation whose progress matches a + * swipe progress. + * + * @param placeholderDuration a placeholder duration to be used to ensure all full-length + * sub-animations are properly coordinated. This duration should not + * actually be used since this animation tracks a swipe progress. + */ + protected void addUnstashToHotseatAnimation(AnimatorSet animation, int placeholderDuration) { + createAnimToIsStashed( + /* isStashed= */ false, + placeholderDuration, + TRANSITION_UNSTASH_SUW_MANUAL); + animation.play(mAnimator); + } + /** * 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 startDelay how many milliseconds to delay the animation after starting it. + * @param animationType what transition type to play. */ - private void createAnimToIsStashed(boolean isStashed, long duration, long startDelay) { + private void createAnimToIsStashed(boolean isStashed, long duration, + @StashAnimation int animationType) { + 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"); + } + if (mAnimator != null) { mAnimator.cancel(); } mAnimator = new AnimatorSet(); + addJankMonitorListener(mAnimator, /* appearing= */ !mIsStashed); + boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); + final float stashTranslation = isPhoneMode() || isTransientTaskbar + ? 0 + : (mUnstashedHeight - mStashedHeight); if (!supportsVisualStashing()) { // Just hide/show the icons and background instead of stashing into a handle. mAnimator.play(mIconAlphaForStash.animateToValue(isStashed ? 0 : 1) .setDuration(duration)); + mAnimator.playTogether(mTaskbarBackgroundOffset.animateToValue(isStashed ? 1 : 0) + .setDuration(duration)); + mAnimator.playTogether(mIconTranslationYForStash.animateToValue(isStashed + ? stashTranslation : 0) + .setDuration(duration)); mAnimator.play(mTaskbarImeBgAlpha.animateToValue( hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration)); - mAnimator.setStartDelay(startDelay); - mAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimator = null; - } - }); + mAnimator.addListener(AnimatorListeners.forEndCallback(() -> mAnimator = null)); return; } + if (isTransientTaskbar) { + createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType); + } else { + createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType); + } + + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mIsStashed = isStashed; + onIsStashedChanged(mIsStashed); + + cancelTimeoutIfExists(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimator = null; + + if (!mIsStashed) { + tryStartTaskbarTimeout(); + } + + // only announce if we are actually animating + if (duration > 0 && isInApp()) { + mControllers.taskbarViewController.announceForAccessibility(); + } + } + }); + } + + private void createAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, + float stashTranslation, @StashAnimation int animationType) { AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); // Not exactly half and may overlap. See [first|second]HalfDurationScale below. AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); @@ -340,34 +685,50 @@ public class TaskbarStashController { if (isStashed) { firstHalfDurationScale = 0.75f; secondHalfDurationScale = 0.5f; - final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f; - fullLengthAnimatorSet.playTogether( - mTaskbarBackgroundOffset.animateToValue(1), - mIconTranslationYForStash.animateToValue(stashTranslation) - ); + fullLengthAnimatorSet.play(mIconTranslationYForStash.animateToValue(stashTranslation)); + fullLengthAnimatorSet.play(mTaskbarBackgroundOffset.animateToValue(1)); + firstHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(0), - mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE) + mIconScaleForStash.animateToValue(isPhoneMode() ? + 0 : STASHED_TASKBAR_SCALE) ); secondHalfAnimatorSet.playTogether( mTaskbarStashedHandleAlpha.animateToValue(1) ); + + if (animationType == TRANSITION_HANDLE_FADE) { + fullLengthAnimatorSet.setInterpolator(INSTANT); + firstHalfAnimatorSet.setInterpolator(INSTANT); + } } else { firstHalfDurationScale = 0.5f; secondHalfDurationScale = 0.75f; fullLengthAnimatorSet.playTogether( - mTaskbarBackgroundOffset.animateToValue(0), mIconScaleForStash.animateToValue(1), - mIconTranslationYForStash.animateToValue(0) - ); + mIconTranslationYForStash.animateToValue(0)); + + final boolean animateBg = animationType != TRANSITION_UNSTASH_SUW_MANUAL; + if (animateBg) { + fullLengthAnimatorSet.play(mTaskbarBackgroundOffset.animateToValue(0)); + } else { + fullLengthAnimatorSet.addListener(AnimatorListeners.forEndCallback( + () -> mTaskbarBackgroundOffset.updateValue(0))); + } + firstHalfAnimatorSet.playTogether( mTaskbarStashedHandleAlpha.animateToValue(0) ); secondHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(1) ); + + if (animationType == TRANSITION_HANDLE_FADE) { + fullLengthAnimatorSet.setInterpolator(FINAL_FRAME); + secondHalfAnimatorSet.setInterpolator(FINAL_FRAME); + } } fullLengthAnimatorSet.play(mControllers.stashedHandleViewController @@ -381,23 +742,111 @@ public class TaskbarStashController { secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); - mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, + as.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, secondHalfAnimatorSet); - mAnimator.setStartDelay(startDelay); - mAnimator.addListener(new AnimatorListenerAdapter() { + + } + + private void createTransientAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, + @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; + final float stashedHandleAlphaTarget = isStashed ? 1 : 0; + + // Timing for the alpha values depend on the animation played + long iconAlphaStartDelay = 0, iconAlphaDuration = 0, stashedHandleAlphaDelay = 0, + stashedHandleAlphaDuration = 0; + if (duration > 0) { + if (animationType == TRANSITION_HANDLE_FADE) { + // When fading, the handle fades in/out at the beginning of the transition with + // TASKBAR_STASH_ALPHA_DURATION. + stashedHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; + // The iconAlphaDuration must be set to duration for the skippable interpolators + // below to work. + iconAlphaDuration = duration; + } else { + iconAlphaStartDelay = TASKBAR_STASH_ALPHA_START_DELAY; + iconAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; + stashedHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; + + if (isStashed) { + if (animationType == TRANSITION_HOME_TO_APP) { + iconAlphaStartDelay = TASKBAR_STASH_ICON_ALPHA_HOME_TO_APP_START_DELAY; + } + stashedHandleAlphaDelay = iconAlphaStartDelay; + stashedHandleAlphaDuration = Math.max(0, duration - iconAlphaStartDelay); + } + + } + } + + play(as, mTaskbarStashedHandleAlpha.animateToValue(stashedHandleAlphaTarget), + stashedHandleAlphaDelay, + stashedHandleAlphaDuration, LINEAR); + + // The rest of the animations might be "skipped" in TRANSITION_HANDLE_FADE transitions. + AnimatorSet skippable = as; + if (animationType == TRANSITION_HANDLE_FADE) { + skippable = new AnimatorSet(); + as.play(skippable); + skippable.setInterpolator(isStashed ? INSTANT : FINAL_FRAME); + } + + final boolean animateBg = animationType != TRANSITION_UNSTASH_SUW_MANUAL; + if (animateBg) { + play(skippable, mTaskbarBackgroundOffset.animateToValue(backgroundOffsetTarget), 0, + duration, EMPHASIZED); + } else { + skippable.addListener(AnimatorListeners.forEndCallback( + () -> mTaskbarBackgroundOffset.updateValue(backgroundOffsetTarget))); + } + + play(skippable, mIconAlphaForStash.animateToValue(iconAlphaTarget), iconAlphaStartDelay, + iconAlphaDuration, + LINEAR); + + if (isStashed) { + play(skippable, mControllers.taskbarSpringOnStashController.createSpringToStash(), + 0, duration, LINEAR); + } + + mControllers.taskbarViewController.addRevealAnimToIsStashed(skippable, isStashed, duration, + EMPHASIZED); + + play(skippable, mControllers.stashedHandleViewController + .createRevealAnimToIsStashed(isStashed), 0, duration, EMPHASIZED); + + // Return the stashed handle to its default scale in case it was changed as part of the + // feedforward hint. Note that the reveal animation above also visually scales it. + skippable.play(mTaskbarStashedHandleHintScale.animateToValue(1f) + .setDuration(isStashed ? duration / 2 : duration)); + } + + private static void play(AnimatorSet as, Animator a, long startDelay, long duration, + Interpolator interpolator) { + a.setDuration(duration); + a.setStartDelay(startDelay); + a.setInterpolator(interpolator); + as.play(a); + } + + private void addJankMonitorListener(AnimatorSet animator, boolean expanding) { + View v = mControllers.taskbarActivityContext.getDragLayer(); + int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND : + InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE; + animator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationStart(Animator animation) { - mIsStashed = isStashed; - onIsStashed(mIsStashed); + public void onAnimationStart(@NonNull Animator animation) { + InteractionJankMonitor.getInstance().begin(v, action); } @Override - public void onAnimationEnd(Animator animation) { - mAnimator = null; + public void onAnimationEnd(@NonNull Animator animation) { + InteractionJankMonitor.getInstance().end(action); } }); } - /** * Creates and starts a partial stash animation, hinting at the new state that will trigger when * long press is detected. @@ -425,33 +874,49 @@ public class TaskbarStashController { // Already unstashed, no need to hint in that direction. return; } + if (!canCurrentlyManuallyUnstash()) { + // If any other flags are causing us to be stashed, long press won't cause us to + // unstash, so don't hint that it will. + return; + } mTaskbarStashedHandleHintScale.animateToValue( animateForward ? UNSTASHED_TASKBAR_HANDLE_HINT_SCALE : 1) .setDuration(TASKBAR_HINT_STASH_DURATION).start(); } - private void onIsStashed(boolean isStashed) { - mControllers.stashedHandleViewController.onIsStashed(isStashed); + private void onIsStashedChanged(boolean isStashed) { + mControllers.runAfterInit(() -> { + mControllers.stashedHandleViewController.onIsStashedChanged(isStashed); + mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged(); + }); } public void applyState() { - applyState(TASKBAR_STASH_DURATION); + applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION); } public void applyState(long duration) { - mStatePropertyHolder.setState(mState, duration, true); + Animator animator = createApplyStateAnimator(duration); + if (animator != null) { + animator.start(); + } } public void applyState(long duration, long startDelay) { - mStatePropertyHolder.setState(mState, duration, startDelay, true); + Animator animator = createApplyStateAnimator(duration); + if (animator != null) { + animator.setStartDelay(startDelay); + animator.start(); + } } - public Animator applyStateWithoutStart() { - return applyStateWithoutStart(TASKBAR_STASH_DURATION); - } - - public Animator applyStateWithoutStart(long duration) { - return mStatePropertyHolder.setState(mState, duration, false); + /** + * Returns an animator which applies the latest state if mIsStashed is changed, or {@code null} + * otherwise. + */ + @Nullable + public Animator createApplyStateAnimator(long duration) { + return mStatePropertyHolder.createSetStateAnimator(mState, duration); } /** @@ -460,10 +925,32 @@ public class TaskbarStashController { */ public void setSystemGestureInProgress(boolean inProgress) { mIsSystemGestureInProgress = inProgress; - // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. - if (!mIsSystemGestureInProgress) { - updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); + if (mIsSystemGestureInProgress) { + return; + } + + // Only update the following flags when system gesture is not in progress. + boolean shouldStashForIme = shouldStashForIme(); + updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false); + if (hasAnyFlag(FLAG_STASHED_IN_APP_IME) != shouldStashForIme) { + updateStateForFlag(FLAG_STASHED_IN_APP_IME, shouldStashForIme); applyState(TASKBAR_STASH_DURATION_FOR_IME, getTaskbarStashStartDelayForIme()); + } else { + applyState(mControllers.taskbarOverlayController.getCloseDuration()); + } + } + + /** + * Resets the flag if no system gesture is in progress. + *

+ * Otherwise, the reset should be deferred until after the gesture is finished. + * + * @see #setSystemGestureInProgress + */ + public void resetFlagIfNoGestureInProgress(int flag) { + if (!mIsSystemGestureInProgress) { + updateStateForFlag(flag, false); + applyState(mControllers.taskbarOverlayController.getCloseDuration()); } } @@ -486,13 +973,21 @@ public class TaskbarStashController { long animDuration = TASKBAR_STASH_DURATION; long startDelay = 0; - updateStateForFlag(FLAG_STASHED_IN_APP_PINNED, + updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags, + SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE)); + updateStateForFlag(FLAG_STASHED_SYSUI, hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING)); + boolean isLocked = hasAnyFlag(systemUiStateFlags, MASK_ANY_SYSUI_LOCKED) + && !hasAnyFlag(systemUiStateFlags, SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY); + updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, isLocked); + // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING); + mIsImeSwitcherShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SWITCHER_SHOWING); + if (!mIsSystemGestureInProgress) { - updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); + updateStateForFlag(FLAG_STASHED_IN_APP_IME, shouldStashForIme()); animDuration = TASKBAR_STASH_DURATION_FOR_IME; startDelay = getTaskbarStashStartDelayForIme(); } @@ -500,6 +995,22 @@ public class TaskbarStashController { applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay); } + /** + * We stash when IME or IME switcher is showing AND NOT + * * in small screen AND + * * 3 button nav AND + * * landscape (or seascape) + * We do not stash if taskbar is transient + */ + private boolean shouldStashForIme() { + if (DisplayController.isTransientTaskbar(mActivity)) { + return false; + } + return (mIsImeShowing || mIsImeSwitcherShowing) && + !(isPhoneMode() && mActivity.isThreeButtonNav() + && mActivity.getDeviceProfile().isLandscape); + } + /** * Updates the proper flag to indicate whether the task bar should be stashed. * @@ -510,6 +1021,10 @@ public class TaskbarStashController { * unstashed. */ public void updateStateForFlag(int flag, boolean enabled) { + if (flag == FLAG_IN_APP && TestProtocol.sDebugTracing) { + Log.d(TestProtocol.TASKBAR_IN_APP_STATE, String.format( + "setting flag FLAG_IN_APP to: %b", enabled), new Exception()); + } if (enabled) { mState |= flag; } else { @@ -525,9 +1040,11 @@ public class TaskbarStashController { if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP)) { mControllers.uiController.onStashedInAppChanged(); } - if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP | FLAG_IN_APP)) { - notifyStashChange(/* visible */ hasAnyFlag(FLAG_IN_APP), + if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP | FLAGS_IN_APP)) { + notifyStashChange(/* visible */ hasAnyFlag(FLAGS_IN_APP), /* stashed */ isStashedInApp()); + mControllers.taskbarAutohideSuspendController.updateFlag( + TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, !isInApp()); } if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_MANUAL)) { if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL)) { @@ -536,57 +1053,251 @@ public class TaskbarStashController { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_SHOW); } } + if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_AUTO)) { + mActivity.getStatsLogManager().logger().log(hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) + ? LAUNCHER_TRANSIENT_TASKBAR_HIDE + : LAUNCHER_TRANSIENT_TASKBAR_SHOW); + } } private void notifyStashChange(boolean visible, boolean stashed) { mSystemUiProxy.notifyTaskbarStatus(visible, stashed); + setUpTaskbarSystemAction(visible); + // If stashing taskbar is caused by IME visibility, we could just skip updating rounded + // corner insets since the rounded corners will be covered by IME during IME is showing and + // taskbar will be restored back to unstashed when IME is hidden. + mControllers.taskbarActivityContext.updateInsetRoundedCornerFrame( + visible && !isStashedInAppIgnoringIme()); mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed); } + /** + * Setup system action for showing Taskbar depending on its visibility. + */ + public void setUpTaskbarSystemAction(boolean visible) { + UI_HELPER_EXECUTOR.execute(() -> { + if (!visible || !DisplayController.isTransientTaskbar(mActivity)) { + mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR); + mIsTaskbarSystemActionRegistered = false; + return; + } + + if (!mIsTaskbarSystemActionRegistered) { + RemoteAction taskbarRemoteAction = new RemoteAction( + Icon.createWithResource(mActivity, R.drawable.ic_info_no_shadow), + mActivity.getString(R.string.taskbar_a11y_title), + mActivity.getString(R.string.taskbar_a11y_title), + mTaskbarSharedState.taskbarSystemActionPendingIntent); + + mAccessibilityManager.registerSystemAction(taskbarRemoteAction, + SYSTEM_ACTION_ID_TASKBAR); + mIsTaskbarSystemActionRegistered = true; + } + }); + } + + /** + * Clean up on destroy from TaskbarControllers + */ + public void onDestroy() { + UI_HELPER_EXECUTOR.execute( + () -> mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR)); + } + + /** + * Cancels a timeout if any exists. + */ + public void cancelTimeoutIfExists() { + if (mTimeoutAlarm.alarmPending()) { + mTimeoutAlarm.cancelAlarm(); + } + } + + /** + * Updates the status of the taskbar timeout. + * @param isAutohideSuspended If true, cancels any existing timeout + * If false, attempts to re/start the timeout + */ + public void updateTaskbarTimeout(boolean isAutohideSuspended) { + if (!DisplayController.isTransientTaskbar(mActivity)) { + return; + } + if (isAutohideSuspended) { + cancelTimeoutIfExists(); + } else { + tryStartTaskbarTimeout(); + } + } + + /** + * Attempts to start timer to auto hide the taskbar based on time. + */ + public void tryStartTaskbarTimeout() { + if (!DisplayController.isTransientTaskbar(mActivity) + || mIsStashed + || mEnableBlockingTimeoutDuringTests) { + return; + } + + cancelTimeoutIfExists(); + + mTimeoutAlarm.setOnAlarmListener(this::onTaskbarTimeout); + mTimeoutAlarm.setAlarm(getTaskbarAutoHideTimeout()); + } + + /** + * returns appropriate timeout for taskbar to stash depending on accessibility being on/off. + */ + private long getTaskbarAutoHideTimeout() { + return mAccessibilityManager.getRecommendedTimeoutMillis((int) NO_TOUCH_TIMEOUT_TO_STASH_MS, + FLAG_CONTENT_CONTROLS); + } + + private void onTaskbarTimeout(Alarm alarm) { + if (mControllers.taskbarAutohideSuspendController.isSuspended()) { + return; + } + updateAndAnimateTransientTaskbar(true); + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarStashController:"); + + pw.println(prefix + "\tmStashedHeight=" + mStashedHeight); + pw.println(prefix + "\tmUnstashedHeight=" + mUnstashedHeight); + pw.println(prefix + "\tmIsStashed=" + mIsStashed); + pw.println(prefix + "\tappliedState=" + getStateString(mStatePropertyHolder.mPrevFlags)); + pw.println(prefix + "\tmState=" + getStateString(mState)); + pw.println(prefix + "\tmIsSystemGestureInProgress=" + mIsSystemGestureInProgress); + pw.println(prefix + "\tmIsImeShowing=" + mIsImeShowing); + pw.println(prefix + "\tmIsImeSwitcherShowing=" + mIsImeSwitcherShowing); + } + + private static String getStateString(int flags) { + StringJoiner sj = new StringJoiner("|"); + appendFlag(sj, flags, FLAGS_IN_APP, "FLAG_IN_APP"); + appendFlag(sj, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL"); + appendFlag(sj, flags, FLAG_STASHED_IN_APP_SYSUI, "FLAG_STASHED_IN_APP_SYSUI"); + appendFlag(sj, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP"); + appendFlag(sj, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME"); + appendFlag(sj, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE"); + appendFlag(sj, flags, FLAG_STASHED_IN_TASKBAR_ALL_APPS, "FLAG_STASHED_IN_TASKBAR_ALL_APPS"); + appendFlag(sj, flags, FLAG_IN_SETUP, "FLAG_IN_SETUP"); + appendFlag(sj, flags, FLAG_STASHED_IN_APP_AUTO, "FLAG_STASHED_IN_APP_AUTO"); + appendFlag(sj, flags, FLAG_STASHED_SYSUI, "FLAG_STASHED_SYSUI"); + appendFlag(sj, flags, FLAG_STASHED_DEVICE_LOCKED, "FLAG_STASHED_DEVICE_LOCKED"); + return sj.toString(); + } + private class StatePropertyHolder { private final IntPredicate mStashCondition; private boolean mIsStashed; + private @StashAnimation int mLastStartedTransitionType = TRANSITION_DEFAULT; private int mPrevFlags; + private long mLastUnlockTransitionTimeout = 0; + StatePropertyHolder(IntPredicate stashCondition) { mStashCondition = stashCondition; } /** - * @see #setState(int, long, long, boolean) with a default startDelay = 0. - */ - public Animator setState(int flags, long duration, boolean start) { - return setState(flags, duration, 0 /* startDelay */, start); - } - - /** - * Applies the latest state, potentially calling onStateChangeApplied() and creating a new - * animation (stored in mAnimator) which is started if {@param start} is true. + * Creates an animator (stored in mAnimator) which applies the latest state, potentially + * creating a new animation (stored in mAnimator). * @param flags The latest flags to apply (see the top of this file). * @param duration The length of the animation. - * @param startDelay How long to delay the animation after calling start(). - * @param start Whether to start mAnimator immediately. - * @return mAnimator if mIsStashed changed, else null. + * @return mAnimator if mIsStashed changed, or {@code null} otherwise. */ - public Animator setState(int flags, long duration, long startDelay, boolean start) { + @Nullable + public Animator createSetStateAnimator(int flags, long duration) { + boolean isStashed = mStashCondition.test(flags); + + if (DEBUG) { + String stateString = formatFlagChange(flags, mPrevFlags, + TaskbarStashController::getStateString); + Log.d(TAG, "createSetStateAnimator: flags: " + stateString + + ", duration: " + duration + + ", isStashed: " + isStashed + + ", mIsStashed: " + mIsStashed); + } + int changedFlags = mPrevFlags ^ flags; if (mPrevFlags != flags) { onStateChangeApplied(changedFlags); mPrevFlags = flags; } - boolean isStashed = mStashCondition.test(flags); - if (mIsStashed != isStashed) { + + boolean isUnlockTransition = hasAnyFlag(changedFlags, FLAG_STASHED_DEVICE_LOCKED) + && !hasAnyFlag(FLAG_STASHED_DEVICE_LOCKED); + if (isUnlockTransition) { + // the launcher might not be resumed at the time the device is considered + // unlocked (when the keyguard goes away), but possibly shortly afterwards. + // To play the unlock transition at the time the unstash animation actually happens, + // this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS. + mLastUnlockTransitionTimeout = + SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS; + } + + @StashAnimation int animationType = computeTransitionType(changedFlags); + + // Allow re-starting animation if upgrading from default animation type, otherwise + // stick with the already started transition. + boolean transitionTypeChanged = mAnimator != null && mAnimator.isStarted() + && mLastStartedTransitionType == TRANSITION_DEFAULT + && animationType != TRANSITION_DEFAULT; + + if (mIsStashed != isStashed || transitionTypeChanged) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.TASKBAR_IN_APP_STATE, String.format( + "setState: mIsStashed=%b, isStashed=%b, " + + "mAnimationType=%d, animationType=%d, duration=%d", + mIsStashed, + isStashed, + mLastStartedTransitionType, + animationType, + duration)); + } mIsStashed = isStashed; + mLastStartedTransitionType = animationType; // This sets mAnimator. - createAnimToIsStashed(mIsStashed, duration, startDelay); - if (start) { - mAnimator.start(); - } + createAnimToIsStashed(mIsStashed, duration, animationType); return mAnimator; } return null; } + + private @StashAnimation int computeTransitionType(int changedFlags) { + + boolean hotseatHiddenDuringAppLaunch = + !mControllers.uiController.isHotseatIconOnTopWhenAligned() + && hasAnyFlag(changedFlags, FLAG_IN_APP); + if (hotseatHiddenDuringAppLaunch) { + // When launching an app from the all-apps drawer, the hotseat is hidden behind the + // drawer. In this case, the navbar must just fade in, without a stash transition, + // as the taskbar stash animation would otherwise be visible above the all-apps + // drawer once the hotseat is detached. + return TRANSITION_HANDLE_FADE; + } + + boolean isUnlockTransition = + SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout; + if (isUnlockTransition) { + // When transitioning to unlocked device, the hotseat will already be visible on + // the homescreen, thus do not play an un-stash animation. + // Keep isUnlockTransition in sync with its counterpart in + // TaskbarLauncherStateController#onStateChangeApplied. + return TRANSITION_HANDLE_FADE; + } + + boolean homeToApp = hasAnyFlag(changedFlags, FLAG_IN_APP) && hasAnyFlag(FLAG_IN_APP); + if (homeToApp) { + return TRANSITION_HOME_TO_APP; + } + + return TRANSITION_DEFAULT; + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt new file mode 100644 index 0000000000..23add74d91 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import android.view.MotionEvent +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.anim.Interpolators.LINEAR +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE +import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.TouchController +import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer +import com.android.systemui.shared.testing.ResourceUtils + +/** + * A helper [TouchController] for [TaskbarDragLayerController], specifically to handle touch events + * to stash Transient Taskbar. There are two cases to handle: + * - A touch outside of Transient Taskbar bounds will immediately stash on [MotionEvent.ACTION_DOWN] + * or [MotionEvent.ACTION_OUTSIDE]. + * - Touches inside Transient Taskbar bounds will stash if it is detected as a swipe down gesture. + * + * Note: touches to *unstash* Taskbar are handled by [TaskbarStashInputConsumer]. + */ +class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController { + + private val activity: TaskbarActivityContext = controllers.taskbarActivityContext + private val enabled = DisplayController.isTransientTaskbar(activity) + private val swipeDownDetector: SingleAxisSwipeDetector + private val translationCallback = controllers.taskbarTranslationController.transitionCallback + /** Interpolator to apply resistance as user swipes down to the bottom of the screen. */ + private val displacementInterpolator = LINEAR + /** How far we can translate the TaskbarView before it's offscreen. */ + private val maxVisualDisplacement = + activity.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat() + /** How far the swipe could go, if user swiped from the very top of TaskbarView. */ + private val maxTouchDisplacement = maxVisualDisplacement + activity.deviceProfile.taskbarHeight + private val touchDisplacementToStash = + activity.resources.getDimensionPixelSize(R.dimen.taskbar_to_nav_threshold).toFloat() + + /** The height of the system gesture region, so we don't stash when touching down there. */ + private var gestureHeightYThreshold = 0f + + init { + updateGestureHeight() + swipeDownDetector = SingleAxisSwipeDetector(activity, createSwipeListener(), VERTICAL) + swipeDownDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false) + } + + fun updateGestureHeight() { + if (!enabled) return + + val gestureHeight: Int = + ResourceUtils.getNavbarSize( + ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, + activity.resources + ) + gestureHeightYThreshold = (activity.deviceProfile.heightPx - gestureHeight).toFloat() + } + + private fun createSwipeListener() = + object : SingleAxisSwipeDetector.Listener { + private var lastDisplacement = 0f + + override fun onDragStart(start: Boolean, startDisplacement: Float) {} + + override fun onDrag(displacement: Float): Boolean { + lastDisplacement = displacement + if (displacement < 0) return false + // Apply resistance so that the visual displacement doesn't go beyond the screen. + translationCallback.onActionMove( + Utilities.mapToRange( + displacement, + 0f, + maxTouchDisplacement, + 0f, + maxVisualDisplacement, + displacementInterpolator + ) + ) + return false + } + + override fun onDragEnd(velocity: Float) { + val isFlingDown = swipeDownDetector.isFling(velocity) && velocity > 0 + val isSignificantDistance = lastDisplacement > touchDisplacementToStash + if (isFlingDown || isSignificantDistance) { + // Successfully triggered stash. + controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) + } + translationCallback.onActionEnd() + swipeDownDetector.finishedScrolling() + } + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if (!enabled || controllers.taskbarStashController.isStashed) { + return false + } + + val screenCoordinatesEv = MotionEvent.obtain(ev) + screenCoordinatesEv.setLocation(ev.rawX, ev.rawY) + if (ev.action == MotionEvent.ACTION_OUTSIDE) { + controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) + } else if (controllers.taskbarViewController.isEventOverAnyItem(screenCoordinatesEv)) { + swipeDownDetector.onTouchEvent(ev) + if (swipeDownDetector.isDraggingState) { + return true + } + } else if (ev.action == MotionEvent.ACTION_DOWN) { + if (screenCoordinatesEv.y < gestureHeightYThreshold) { + controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) + } + } + return false + } + + override fun onControllerTouchEvent(ev: MotionEvent) = swipeDownDetector.onTouchEvent(ev) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java new file mode 100644 index 0000000000..4b18bb6556 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java @@ -0,0 +1,240 @@ +/* + * 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; + +import static com.android.launcher3.anim.AnimatedFloat.VALUE; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; + +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.util.DisplayController; + +import java.io.PrintWriter; + +/** + * Class responsible for translating the transient taskbar UI during a swipe gesture. + * + * The translation is controlled, in priority order: + * - animation to home + * - a spring animation + * - controlled by user + * + * The spring animation will play start once the user lets go or when user pauses to go to overview. + * When the user goes home, the stash animation will play. + */ +public class TaskbarTranslationController implements TaskbarControllers.LoggableTaskbarController { + + private final TaskbarActivityContext mContext; + private TaskbarControllers mControllers; + private final AnimatedFloat mTranslationYForSwipe = new AnimatedFloat( + this::updateTranslationYForSwipe); + + private boolean mHasSprungOnceThisGesture; + private @Nullable ValueAnimator mSpringBounce; + private boolean mGestureEnded; + private boolean mGestureInProgress; + private boolean mAnimationToHomeRunning; + + private final boolean mIsTransientTaskbar; + + private final TransitionCallback mCallback; + + public TaskbarTranslationController(TaskbarActivityContext context) { + mContext = context; + mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext); + mCallback = new TransitionCallback(); + } + + /** + * Initialization method. + */ + public void init(TaskbarControllers controllers) { + mControllers = controllers; + } + + /** + * Called to cancel any existing animations. + */ + public void cancelSpringIfExists() { + if (mSpringBounce != null) { + mSpringBounce.cancel(); + mSpringBounce = null; + } + } + + private void updateTranslationYForSwipe() { + if (!mIsTransientTaskbar) { + return; + } + + float transY = mTranslationYForSwipe.value; + mControllers.stashedHandleViewController.setTranslationYForSwipe(transY); + mControllers.taskbarViewController.setTranslationYForSwipe(transY); + mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY); + } + + /** + * Starts a spring aniamtion to set the views back to the resting state. + */ + public void startSpring() { + if (mHasSprungOnceThisGesture || mAnimationToHomeRunning) { + return; + } + mSpringBounce = new SpringAnimationBuilder(mContext) + .setStartValue(mTranslationYForSwipe.value) + .setEndValue(0) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .build(mTranslationYForSwipe, VALUE); + mSpringBounce.addListener(forEndCallback(() -> { + if (!mGestureEnded) { + return; + } + reset(); + if (mControllers.taskbarStashController.isInApp() + && mControllers.taskbarStashController.isTaskbarVisibleAndNotStashing()) { + mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); + } + })); + mSpringBounce.start(); + mHasSprungOnceThisGesture = true; + } + + private void reset() { + mGestureEnded = false; + mGestureInProgress = false; + mHasSprungOnceThisGesture = false; + } + + /** + * Returns a callback to help monitor the swipe gesture. + */ + public TransitionCallback getTransitionCallback() { + return mCallback; + } + + /** + * Returns {@code true} if we should reset the animation back to zero. + * + * Returns {@code false} if there is a gesture in progress, or if we are already animating + * to 0 within the specified duration. + */ + public boolean shouldResetBackToZero(long duration) { + if (mGestureInProgress) { + return false; + } + if (mSpringBounce != null && mSpringBounce.isRunning()) { + long springDuration = mSpringBounce.getDuration(); + long current = mSpringBounce.getCurrentPlayTime(); + return (springDuration - current >= duration); + } + if (mTranslationYForSwipe.isAnimatingToValue(0)) { + return mTranslationYForSwipe.getRemainingTime() >= duration; + } + return true; + } + + /** + * Returns an animation to reset the taskbar translation to {@code 0}. + */ + public ObjectAnimator createAnimToResetTranslation(long duration) { + ObjectAnimator animator = mTranslationYForSwipe.animateToValue(0); + animator.setInterpolator(Interpolators.LINEAR); + animator.setDuration(duration); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + cancelSpringIfExists(); + reset(); + mAnimationToHomeRunning = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimationToHomeRunning = false; + reset(); + } + }); + return animator; + } + + /** + * Helper class to communicate to/from the input consumer. + */ + public class TransitionCallback { + + /** + * Clears any existing animations so that user + * can take control over the movement of the taskbaer. + */ + public void onActionDown() { + if (mAnimationToHomeRunning) { + mTranslationYForSwipe.cancelAnimation(); + } + mAnimationToHomeRunning = false; + cancelSpringIfExists(); + reset(); + mGestureInProgress = true; + } + /** + * Called when there is movement to move the taskbar. + */ + public void onActionMove(float dY) { + if (mAnimationToHomeRunning + || (mHasSprungOnceThisGesture && !mGestureEnded)) { + return; + } + + mTranslationYForSwipe.updateValue(dY); + } + + /** + * Called when swipe gesture has ended. + */ + public void onActionEnd() { + if (mHasSprungOnceThisGesture) { + reset(); + } else { + mGestureEnded = true; + startSpring(); + } + mGestureInProgress = false; + } + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarTranslationController:"); + + pw.println(prefix + "\tmTranslationYForSwipe=" + mTranslationYForSwipe.value); + pw.println(prefix + "\tmHasSprungOnceThisGesture=" + mHasSprungOnceThisGesture); + pw.println(prefix + "\tmAnimationToHomeRunning=" + mAnimationToHomeRunning); + pw.println(prefix + "\tmGestureEnded=" + mGestureEnded); + pw.println(prefix + "\tmGestureInProgress=" + mGestureInProgress); + pw.println(prefix + "\tmSpringBounce is running=" + (mSpringBounce != null + && mSpringBounce.isRunning())); + } +} + diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index abad9060ab..1435cb0996 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -15,14 +15,32 @@ */ package com.android.launcher3.taskbar; +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 android.content.Intent; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; import android.view.View; import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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.ComponentKey; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; -import java.util.stream.Stream; +import java.io.PrintWriter; /** * Base class for providing different taskbar UI @@ -48,13 +66,30 @@ public class TaskbarUIController { return true; } - protected void onStashedInAppChanged() { } - - public Stream getAppIconsForEdu() { - return Stream.empty(); + /** + * This should only be called by TaskbarStashController so that a TaskbarUIController can + * disable stashing. All other controllers should use + * {@link TaskbarStashController#supportsVisualStashing()} as the source of truth. + */ + public boolean supportsVisualStashing() { + return true; } - public void onTaskbarIconLaunched(WorkspaceItemInfo item) { } + protected void onStashedInAppChanged() { } + + /** + * Called when taskbar icon layout bounds change. + */ + protected void onIconLayoutBoundsChanged() { } + + /** 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. + mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); + mControllers.taskbarStashController.applyState(); + } public View getRootView() { return mControllers.taskbarActivityContext.getDragLayer(); @@ -67,4 +102,220 @@ public class TaskbarUIController { public void setSystemGestureInProgress(boolean inProgress) { mControllers.taskbarStashController.setSystemGestureInProgress(inProgress); } + + /** + * Manually closes the overlay window. + */ + public void hideOverlayWindow() { + if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext) + || mControllers.taskbarAllAppsController.isOpen()) { + mControllers.taskbarOverlayController.hideWindow(); + } + } + + /** + * User expands PiP to full-screen (or split-screen) mode, try to hide the Taskbar. + */ + public void onExpandPip() { + if (mControllers != null) { + final TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_IN_APP, true); + stashController.applyState(); + } + } + + /** + * SysUI flags updated, see QuickStepContract.SYSUI_STATE_* values. + */ + public void updateStateForSysuiFlags(int sysuiFlags, boolean skipAnim){ + } + + /** + * Returns {@code true} iff taskbar is stashed. + */ + public boolean isTaskbarStashed() { + return mControllers.taskbarStashController.isStashed(); + } + + /** + * Returns {@code true} iff taskbar All Apps is open. + */ + public boolean isTaskbarAllAppsOpen() { + return mControllers.taskbarAllAppsController.isOpen(); + } + + /** + * Called at the end of the swipe gesture on Transient taskbar. + */ + public void startTranslationSpring() { + mControllers.taskbarActivityContext.startTranslationSpring(); + } + + /* + * @param ev MotionEvent in screen coordinates. + * @return Whether any Taskbar item could handle the given MotionEvent if given the chance. + */ + public boolean isEventOverAnyTaskbarItem(MotionEvent ev) { + return mControllers.taskbarViewController.isEventOverAnyItem(ev) + || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev); + } + + /** + * Returns true if icons should be aligned to hotseat in the current transition. + */ + public boolean isIconAlignedWithHotseat() { + return false; + } + + /** + * Returns true if hotseat icons are on top of view hierarchy when aligned in the current state. + */ + public boolean isHotseatIconOnTopWhenAligned() { + return true; + } + + /** Returns {@code true} if Taskbar is currently within overview. */ + protected boolean isInOverview() { + return false; + } + + @CallSuper + protected void dumpLogs(String prefix, PrintWriter pw) { + pw.println(String.format( + "%sTaskbarUIController: using an instance of %s", + prefix, + getClass().getSimpleName())); + } + + /** + * Returns RecentsView. Overwritten in LauncherTaskbarUIController and + * FallbackTaskbarUIController with Launcher-specific implementations. Returns null for other + * UI controllers (like DesktopTaskbarUIController) that don't have a RecentsView. + */ + public @Nullable RecentsView getRecentsView() { + return null; + } + + public void startSplitSelection(SplitConfigurationOptions.SplitSelectSource splitSelectSource) { + RecentsView recentsView = getRecentsView(); + if (recentsView == null) { + return; + } + + ComponentKey componentToBeStaged = new ComponentKey( + splitSelectSource.itemInfo.getTargetComponent(), + splitSelectSource.itemInfo.user); + recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback( + componentToBeStaged, + foundTask -> { + splitSelectSource.alreadyRunningTaskId = foundTask == null + ? INVALID_TASK_ID + : foundTask.key.id; + splitSelectSource.animateCurrentTaskDismissal = foundTask != null; + recentsView.initiateSplitSelect(splitSelectSource); + } + ); + } + + /** + * Uses the clicked Taskbar icon to launch a second app for splitscreen. + */ + public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) { + RecentsView recents = getRecentsView(); + ComponentKey secondAppComponent = new ComponentKey(info.getTargetComponent(), info.user); + recents.getSplitSelectController().findLastActiveTaskAndRunCallback( + secondAppComponent, + foundTask -> { + if (foundTask != null) { + TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); + // TODO (b/266482558): This additional null check is needed because there + // are times when our Tasks list doesn't match our TaskViews list (like when + // a tile is removed during {@link RecentsView#applyLoadPlan()}. A clearer + // state management system is in the works so that we don't need to rely on + // null checks as much. See comments at ag/21152798. + if (foundTaskView != null) { + // There is already a running app of this type, use that as second app. + // Get index of task (0 or 1), in case it's a GroupedTaskView + TaskIdAttributeContainer taskAttributes = + foundTaskView.getTaskAttributesById(foundTask.key.id); + recents.confirmSplitSelect( + foundTaskView, + foundTask, + taskAttributes.getIconView().getDrawable(), + taskAttributes.getThumbnailView(), + taskAttributes.getThumbnailView().getThumbnail(), + null /* intent */, + null /* user */); + return; + } + } + + // No running app of that type, create a new instance as second app. + recents.confirmSplitSelect( + null /* containerTaskView */, + null /* task */, + new BitmapDrawable(info.bitmap.icon), + startingView, + null /* thumbnail */, + intent, + info.user); + } + ); + } + + /** + * Opens the Keyboard Quick Switch View. + * + * This will set the focus to the first task from the right (from the left in RTL) + */ + public void openQuickSwitchView() { + mControllers.keyboardQuickSwitchController.openQuickSwitchView(); + } + + /** + * Launches the focused task and closes the Keyboard Quick Switch View. + * + * If the overlay or view are closed, or the overview task is focused, then Overview is + * launched. If the overview task is launched, then the first hidden task is focused. + * + * @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched + */ + public int launchFocusedTask() { + int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask(); + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(); + return focusedTaskIndex; + } + + /** + * Launches the focused task in splitscreen. + * + * No-op if the view is not yet open. + */ + public void launchSplitTasks(@NonNull View taskview, @NonNull GroupTask groupTask) { } + + /** + * Returns the matching view (if any) in the taskbar. + * @param view The view to match. + */ + public @Nullable View findMatchingView(View view) { + if (!(view.getTag() instanceof ItemInfo)) { + return null; + } + ItemInfo info = (ItemInfo) view.getTag(); + if (info.container != CONTAINER_HOTSEAT && info.container != CONTAINER_HOTSEAT_PREDICTION) { + return null; + } + + // Taskbar has the same items as the hotseat and we can use screenId to find the match. + int screenId = info.screenId; + View[] views = mControllers.taskbarViewController.getIconViews(); + for (int i = views.length - 1; i >= 0; --i) { + if (views[i] != null + && views[i].getTag() instanceof ItemInfo + && ((ItemInfo) views[i].getTag()).screenId == screenId) { + return views[i]; + } + } + return null; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java index c785186446..280e1debb8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java @@ -21,42 +21,63 @@ import android.view.WindowManager; import com.android.quickstep.util.LauncherViewsMoveFromCenterTranslationApplier; import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator; import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener; +import com.android.systemui.unfold.updates.RotationChangeProvider; +import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; +import java.io.PrintWriter; + /** * Controls animation of taskbar icons when unfolding foldable devices */ -public class TaskbarUnfoldAnimationController { +public class TaskbarUnfoldAnimationController implements + TaskbarControllers.LoggableTaskbarController { - private final ScopedUnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider; + private final ScopedUnfoldTransitionProgressProvider mScopedUnfoldTransitionProgressProvider; + private final NaturalRotationUnfoldProgressProvider mNaturalUnfoldTransitionProgressProvider; private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimator; private final TransitionListener mTransitionListener = new TransitionListener(); private TaskbarViewController mTaskbarViewController; - public TaskbarUnfoldAnimationController(ScopedUnfoldTransitionProgressProvider - unfoldTransitionProgressProvider, WindowManager windowManager) { - mUnfoldTransitionProgressProvider = unfoldTransitionProgressProvider; + public TaskbarUnfoldAnimationController(BaseTaskbarContext context, + ScopedUnfoldTransitionProgressProvider source, + WindowManager windowManager, + RotationChangeProvider rotationChangeProvider) { + mScopedUnfoldTransitionProgressProvider = source; + mNaturalUnfoldTransitionProgressProvider = + new NaturalRotationUnfoldProgressProvider(context, + rotationChangeProvider, + source); mMoveFromCenterAnimator = new UnfoldMoveFromCenterAnimator(windowManager, new LauncherViewsMoveFromCenterTranslationApplier()); } /** * Initializes the controller + * * @param taskbarControllers references to all other taskbar controllers */ public void init(TaskbarControllers taskbarControllers) { + mNaturalUnfoldTransitionProgressProvider.init(); mTaskbarViewController = taskbarControllers.taskbarViewController; mTaskbarViewController.addOneTimePreDrawListener(() -> - mUnfoldTransitionProgressProvider.setReadyToHandleTransition(true)); - mUnfoldTransitionProgressProvider.addCallback(mTransitionListener); + mScopedUnfoldTransitionProgressProvider.setReadyToHandleTransition(true)); + mNaturalUnfoldTransitionProgressProvider.addCallback(mTransitionListener); } /** * Destroys the controller */ public void onDestroy() { - mUnfoldTransitionProgressProvider.setReadyToHandleTransition(false); - mUnfoldTransitionProgressProvider.removeCallback(mTransitionListener); + mScopedUnfoldTransitionProgressProvider.setReadyToHandleTransition(false); + mNaturalUnfoldTransitionProgressProvider.removeCallback(mTransitionListener); + mNaturalUnfoldTransitionProgressProvider.destroy(); + mTaskbarViewController = null; + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarUnfoldAnimationController:"); } private class TransitionListener implements TransitionProgressListener { @@ -84,5 +105,9 @@ public class TaskbarUnfoldAnimationController { public void onTransitionProgress(float progress) { mMoveFromCenterAnimator.onTransitionProgress(progress); } + + @Override + public void onTransitionFinishing() { + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 59393d7b5b..fedeec88e4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -15,13 +15,19 @@ */ package com.android.launcher3.taskbar; +import static android.content.pm.PackageManager.FEATURE_PC; +import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; + import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import androidx.annotation.LayoutRes; @@ -29,26 +35,45 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.icons.ThemedIconDrawable; 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.uioverrides.ApiWrapper; +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.DoubleShadowBubbleTextView; +import com.android.launcher3.views.IconButtonView; +import com.patrykmichalik.opto.core.PreferenceExtensionsKt; + +import java.util.function.Predicate; + +import app.lawnchair.hotseat.HotseatMode; +import app.lawnchair.preferences2.PreferenceManager2; /** * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps. */ -public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable { +public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable, + DeviceProfile.OnDeviceProfileChangeListener { + private static final String TAG = TaskbarView.class.getSimpleName(); + + private static final Rect sTmpRect = new Rect(); private final int[] mTempOutLocation = new int[2]; - - private final Rect mIconLayoutBounds = new Rect(); + private final Rect mIconLayoutBounds; private final int mIconTouchSize; private final int mItemMarginLeftRight; private final int mItemPadding; + private final int mFolderLeaveBehindColor; + private final boolean mIsRtl; private final TaskbarActivityContext mActivityContext; @@ -57,12 +82,23 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private View.OnClickListener mIconClickListener; private View.OnLongClickListener mIconLongClickListener; - // Prevents dispatching touches to children if true - private boolean mTouchEnabled = true; - // Only non-null when the corresponding Folder is open. private @Nullable FolderIcon mLeaveBehindFolderIcon; + // Only non-null when device supports having an All Apps button. + private @Nullable IconButtonView mAllAppsButton; + + // Only non-null when device supports having an All Apps button. + private @Nullable View mTaskbarDivider; + + private View mQsb; + + private float mTransientTaskbarMinWidth; + + private float mTransientTaskbarAllAppsButtonTranslationXOffset; + + private boolean mShouldTryStartAlign; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -72,27 +108,112 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { + int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + PreferenceManager2 preferenceManager2 = PreferenceManager2.getInstance(context); + HotseatMode hotseatMode = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getHotseatMode()); mActivityContext = ActivityContext.lookupContext(context); - + mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds(); Resources resources = getResources(); - mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size); + boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext); + mIsRtl = Utilities.isRtl(resources); + mTransientTaskbarMinWidth = context.getResources().getDimension( + R.dimen.transient_taskbar_min_width); + mTransientTaskbarAllAppsButtonTranslationXOffset = + resources.getDimension(isTransientTaskbar + ? R.dimen.transient_taskbar_all_apps_button_translation_x_offset + : R.dimen.taskbar_all_apps_button_translation_x_offset); + + onDeviceProfileChanged(mActivityContext.getDeviceProfile()); int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing); - int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx; + int actualIconSize = mActivityContext.getDeviceProfile().taskbarIconSize; + + mIconTouchSize = Math.max(actualIconSize, + resources.getDimensionPixelSize(R.dimen.taskbar_icon_min_touch_size)); // We layout the icons to be of mIconTouchSize in width and height mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2; mItemPadding = (mIconTouchSize - actualIconSize) / 2; + mFolderLeaveBehindColor = Themes.getAttrColor(mActivityContext, + android.R.attr.textColorTertiary); + // Needed to draw folder leave-behind when opening one. setWillNotDraw(false); + + if (!mActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) { + mAllAppsButton = (IconButtonView) LayoutInflater.from(context) + .inflate(R.layout.taskbar_all_apps_button, this, false); + mAllAppsButton.setIconDrawable(resources.getDrawable(isTransientTaskbar + ? R.drawable.ic_transient_taskbar_all_apps_button + : R.drawable.ic_taskbar_all_apps_button)); + mAllAppsButton.setScaleX(mIsRtl ? -1 : 1); + mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + mAllAppsButton.setForegroundTint( + mActivityContext.getColor(R.color.all_apps_button_color)); + + if (FeatureFlags.ENABLE_TASKBAR_PINNING.get()) { + mTaskbarDivider = LayoutInflater.from(context).inflate(R.layout.taskbar_divider, + this, false); + } + } + + // TODO: Disable touch events on QSB otherwise it can crash. + if (hotseatMode.isAvailable (context)) { + mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); + } else { + mQsb = LayoutInflater.from(context).inflate(R.layout.empty_view, this, false); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mActivityContext.addOnDeviceProfileChangeListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mActivityContext.removeOnDeviceProfileChangeListener(this); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar; + } + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) { + announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title)); + } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) { + announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title)); + } + return super.performAccessibilityActionInternal(action, arguments); + + } + + protected void announceAccessibilityChanges() { + this.performAccessibilityAction( + isVisibleToUser() ? AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS + : AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); + + ActivityContext.lookupContext(getContext()).getDragLayer() + .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the icon touch size. + */ + public int getIconTouchSize() { + return mIconTouchSize; } protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) { @@ -101,6 +222,13 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener(); setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener()); + + if (mAllAppsButton != null) { + mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener()); + } + if (mTaskbarDivider != null) { + //TODO(b/265434705): set long press listener + } } private void removeAndRecycle(View view) { @@ -120,6 +248,16 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar int nextViewIndex = 0; int numViewsAnimated = 0; + if (mAllAppsButton != null) { + removeView(mAllAppsButton); + + if (mTaskbarDivider != null) { + removeView(mTaskbarDivider); + } + } + removeView(mQsb); + + for (int i = 0; i < hotseatItemInfos.length; i++) { ItemInfo hotseatItemInfo = hotseatItemInfos[i]; if (hotseatItemInfo == null) { @@ -190,6 +328,37 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar while (nextViewIndex < getChildCount()) { removeAndRecycle(getChildAt(nextViewIndex)); } + + if (mAllAppsButton != null) { + mAllAppsButton.setTranslationXForTaskbarAllAppsIcon(getChildCount() > 0 + ? mTransientTaskbarAllAppsButtonTranslationXOffset : 0f); + addView(mAllAppsButton, mIsRtl ? getChildCount() : 0); + + // if only all apps button present, don't include divider view. + if (mTaskbarDivider != null && getChildCount() > 1) { + addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1); + } + } + if (mActivityContext.getDeviceProfile().isQsbInline) { + addView(mQsb, mIsRtl ? getChildCount() : 0); + // Always set QSB to invisible after re-adding. + mQsb.setVisibility(View.INVISIBLE); + } + } + + /** + * Traverse all the child views and change the background of themeIcons + **/ + public void setThemedIconsBackgroundColor(int color) { + for (View icon : getIconViews()) { + if (icon instanceof DoubleShadowBubbleTextView) { + DoubleShadowBubbleTextView textView = ((DoubleShadowBubbleTextView) icon); + if (textView.getIcon() != null + && textView.getIcon() instanceof ThemedIconDrawable) { + ((ThemedIconDrawable) textView.getIcon()).canApplyTheme(); + } + } + } } /** @@ -203,48 +372,95 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int count = getChildCount(); - int spaceNeeded = count * (mItemMarginLeftRight * 2 + mIconTouchSize); - int navSpaceNeeded = ApiWrapper.getHotseatEndOffset(getContext()); - boolean layoutRtl = isLayoutRtl(); - int iconEnd = right - (right - left - spaceNeeded) / 2; - boolean needMoreSpaceForNav = layoutRtl ? - navSpaceNeeded > (iconEnd - spaceNeeded) : - iconEnd > (right - navSpaceNeeded); - if (needMoreSpaceForNav) { - int offset = layoutRtl ? - navSpaceNeeded - (iconEnd - spaceNeeded) : - (right - navSpaceNeeded) - iconEnd; - iconEnd += offset; + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + int spaceNeeded = getIconLayoutWidth(); + // We are removing the margin from taskbar divider item in taskbar, + // so remove it from spacing also. + if (FeatureFlags.ENABLE_TASKBAR_PINNING.get() && count > 1) { + spaceNeeded -= mIconTouchSize; } + int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; + boolean layoutRtl = isLayoutRtl(); + int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2; + int iconEnd; + + if (mShouldTryStartAlign) { + // Taskbar is aligned to the start + int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx; + + if (layoutRtl) { + iconEnd = right - startSpacingPx; + } else { + iconEnd = startSpacingPx + spaceNeeded; + } + } 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; + } + + sTmpRect.set(mIconLayoutBounds); + // Layout the children mIconLayoutBounds.right = iconEnd; mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2; mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize; for (int i = count; i > 0; i--) { View child = getChildAt(i - 1); - iconEnd -= mItemMarginLeftRight; - int iconStart = iconEnd - mIconTouchSize; - child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); - iconEnd = iconStart - mItemMarginLeftRight; + if (child == mQsb) { + int qsbStart; + int qsbEnd; + if (layoutRtl) { + qsbStart = iconEnd + mItemMarginLeftRight; + qsbEnd = qsbStart + deviceProfile.hotseatQsbWidth; + } else { + qsbEnd = iconEnd - mItemMarginLeftRight; + qsbStart = qsbEnd - deviceProfile.hotseatQsbWidth; + } + int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2; + int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight; + child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom); + } else if (child == mTaskbarDivider) { + iconEnd += mItemMarginLeftRight; + int iconStart = iconEnd - mIconTouchSize; + child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); + iconEnd = iconStart + mItemMarginLeftRight; + } else { + iconEnd -= mItemMarginLeftRight; + int iconStart = iconEnd - mIconTouchSize; + child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); + iconEnd = iconStart - mItemMarginLeftRight; + } } - mIconLayoutBounds.left = iconEnd; - } - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (!mTouchEnabled) { - return true; + mIconLayoutBounds.left = iconEnd; + + if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) { + int center = mIconLayoutBounds.centerX(); + int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2; + mIconLayoutBounds.right = center + distanceFromCenter; + mIconLayoutBounds.left = center - distanceFromCenter; + } + + if (!sTmpRect.equals(mIconLayoutBounds)) { + mControllerCallbacks.notifyIconLayoutBoundsChanged(); } - return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { - if (!mTouchEnabled) { - return true; - } - if (mIconLayoutBounds.contains((int) event.getX(), (int) event.getY())) { - // Don't allow long pressing between icons. + if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) { + // Don't allow long pressing between icons, or above/below them. return true; } if (mControllerCallbacks.onTouchEvent(event)) { @@ -259,10 +475,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return super.onTouchEvent(event); } - public void setTouchesEnabled(boolean touchEnabled) { - this.mTouchEnabled = touchEnabled; - } - /** * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's * touch bounds. @@ -278,6 +490,18 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return mIconLayoutBounds; } + /** + * Returns the space used by the icons + */ + public int getIconLayoutWidth() { + int countExcludingQsb = getChildCount(); + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + if (deviceProfile.isQsbInline) { + countExcludingQsb--; + } + return countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize); + } + /** * Returns the app icons currently shown in the taskbar. */ @@ -290,6 +514,21 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return icons; } + /** + * Returns the all apps button in the taskbar. + */ + @Nullable + public View getAllAppsButtonView() { + return mAllAppsButton; + } + + /** + * Returns the QSB in the taskbar. + */ + public View getQsb() { + return mQsb; + } + // FolderIconParent implemented methods. @Override @@ -312,7 +551,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (mLeaveBehindFolderIcon != null) { canvas.save(); canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop()); - mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas); + mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas, + mFolderLeaveBehindColor); canvas.restore(); } } @@ -330,4 +570,39 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // Consider the overall visibility return getVisibility() == VISIBLE; } + + /** + * Maps {@code op} over all the child views. + */ + public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { + // 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)) { + return; + } + } + } + + /** + * Finds the first icon to match one of the given matchers, from highest to lowest priority. + * + * @return The first match, or All Apps button if no match was found. + */ + public View getFirstMatch(Predicate... matchers) { + for (Predicate matcher : matchers) { + for (int i = 0; i < getChildCount(); i++) { + View item = getChildAt(i); + if (!(item.getTag() instanceof ItemInfo)) { + // Should only happen for All Apps button. + continue; + } + ItemInfo info = (ItemInfo) item.getTag(); + if (matcher.test(info)) { + return item; + } + } + } + return mAllAppsButton; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 445a23bf3f..6eb409e35d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -16,32 +16,65 @@ package com.android.launcher3.taskbar; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; +import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.Utilities.squaredHypot; +import static com.android.launcher3.anim.AnimatedFloat.VALUE; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; +import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.quickstep.AnimatedFloat.VALUE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP; +import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode; +import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; import android.graphics.Rect; -import android.util.FloatProperty; +import android.util.Log; import android.view.MotionEvent; import android.view.View; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.animation.Interpolator; + +import androidx.annotation.Nullable; +import androidx.core.graphics.ColorUtils; +import androidx.core.view.OneShotPreDrawListener; -import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.Reorderable; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AlphaUpdateListener; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.anim.RevealOutlineAnimation; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.icons.ThemedIconDrawable; import com.android.launcher3.model.data.ItemInfo; +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.quickstep.AnimatedFloat; + +import java.io.PrintWriter; +import java.util.function.Predicate; /** * Handles properties/data collection, then passes the results to TaskbarView to render. */ -public class TaskbarViewController { +public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController { + + private static final String TAG = TaskbarViewController.class.getSimpleName(); + private static final Runnable NO_OP = () -> { }; public static final int ALPHA_INDEX_HOME = 0; @@ -49,7 +82,11 @@ public class TaskbarViewController { public static final int ALPHA_INDEX_STASH = 2; public static final int ALPHA_INDEX_RECENTS_DISABLED = 3; public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4; - private static final int NUM_ALPHA_CHANNELS = 5; + 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; + + private static final float TASKBAR_DARK_THEME_ICONS_BACKGROUND_LUMINANCE = 0.30f; private final TaskbarActivityContext mActivity; private final TaskbarView mTaskbarView; @@ -60,6 +97,18 @@ public class TaskbarViewController { private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat( this::updateTranslationY); private AnimatedFloat mTaskbarNavButtonTranslationY; + private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay; + private float mTaskbarIconTranslationYForSwipe; + private float mTaskbarIconTranslationYForSpringOnStash; + + private final int mTaskbarBottomMargin; + private final int mStashedHandleHeight; + private final int mLauncherThemedIconsBackgroundColor; + private final int mTaskbarThemedIconsBackgroundColor; + + /** Progress from {@code 0} for Launcher's color to {@code 1} for Taskbar's color. */ + private final AnimatedFloat mThemedIconsBackgroundProgress = new AnimatedFloat( + this::updateIconsBackground); private final TaskbarModelCallbacks mModelCallbacks; @@ -71,18 +120,41 @@ public class TaskbarViewController { private AnimatorPlaybackController mIconAlignControllerLazy = null; private Runnable mOnControllerPreCreateCallback = NO_OP; + private boolean mIsHotseatIconOnTopWhenAligned; + + private final DeviceProfile.OnDeviceProfileChangeListener mDeviceProfileChangeListener = + dp -> commitRunningAppsToUI(); + + private final boolean mIsRtl; + public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) { mActivity = activity; mTaskbarView = taskbarView; mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS); mTaskbarIconAlpha.setUpdateVisibility(true); mModelCallbacks = new TaskbarModelCallbacks(activity, mTaskbarView); + mTaskbarBottomMargin = activity.getDeviceProfile().taskbarBottomMargin; + mStashedHandleHeight = activity.getResources() + .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height); + mLauncherThemedIconsBackgroundColor = ThemedIconDrawable.getColors(mActivity)[0]; + if (!Utilities.isDarkTheme(mActivity)) { + mTaskbarThemedIconsBackgroundColor = mLauncherThemedIconsBackgroundColor; + } else { + // Increase luminance for dark themed icons given they are on a dark Taskbar background. + float[] colorHSL = new float[3]; + ColorUtils.colorToHSL(mLauncherThemedIconsBackgroundColor, colorHSL); + colorHSL[2] = TASKBAR_DARK_THEME_ICONS_BACKGROUND_LUMINANCE; + mTaskbarThemedIconsBackgroundColor = ColorUtils.HSLToColor(colorHSL); + } + mIsRtl = Utilities.isRtl(mTaskbarView.getResources()); } public void init(TaskbarControllers controllers) { mControllers = controllers; mTaskbarView.init(new TaskbarViewCallbacks()); - mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize; + mTaskbarView.getLayoutParams().height = isPhoneMode(mActivity.getDeviceProfile()) + ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_size) + : mActivity.getDeviceProfile().taskbarHeight; mTaskbarIconScaleForStash.updateValue(1f); @@ -93,33 +165,39 @@ public class TaskbarViewController { } mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); + mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController + .getTaskbarNavButtonTranslationYForInAppDisplay(); + + mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener); + } + + /** + * Announcement for Accessibility when Taskbar stashes/unstashes. + */ + public void announceForAccessibility() { + mTaskbarView.announceAccessibilityChanges(); } public void onDestroy() { LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); + mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener); + mModelCallbacks.unregisterListeners(); } public boolean areIconsVisible() { return mTaskbarView.areIconsVisible(); } - public MultiValueAlpha getTaskbarIconAlpha() { + public MultiPropertyFactory getTaskbarIconAlpha() { return mTaskbarIconAlpha; } /** - * Should be called when the IME visibility changes, so we can make Taskbar not steal touches. - */ - public void setImeIsVisible(boolean isImeVisible) { - mTaskbarView.setTouchesEnabled(!isImeVisible); - } - - /** - * Should be called when the recents button is disabled, so we can hide taskbar icons as well. + * Should be called when the recents button is disabled, so we can hide Taskbar icons as well. */ public void setRecentsButtonDisabled(boolean isDisabled) { // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha. - mTaskbarIconAlpha.getProperty(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1); + mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1); } /** @@ -130,32 +208,31 @@ public class TaskbarViewController { } /** - * Adds one time pre draw listener to the taskbar view, it is called before + * Adds one time pre draw listener to the Taskbar view, it is called before * drawing a frame and invoked only once * @param listener callback that will be invoked before drawing the next frame */ - public void addOneTimePreDrawListener(Runnable listener) { - mTaskbarView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - final ViewTreeObserver viewTreeObserver = mTaskbarView.getViewTreeObserver(); - if (viewTreeObserver.isAlive()) { - listener.run(); - viewTreeObserver.removeOnPreDrawListener(this); - } - return true; - } - }); + public void addOneTimePreDrawListener(@NonNull Runnable listener) { + OneShotPreDrawListener.add(mTaskbarView, listener); } public Rect getIconLayoutBounds() { return mTaskbarView.getIconLayoutBounds(); } + public int getIconLayoutWidth() { + return mTaskbarView.getIconLayoutWidth(); + } + public View[] getIconViews() { return mTaskbarView.getIconViews(); } + @Nullable + public View getAllAppsButtonView() { + return mTaskbarView.getAllAppsButtonView(); + } + public AnimatedFloat getTaskbarIconScaleForStash() { return mTaskbarIconScaleForStash; } @@ -173,19 +250,169 @@ public class TaskbarViewController { mTaskbarView.setScaleY(scale); } - private void updateTranslationY() { - mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value - + mTaskbarIconTranslationYForStash.value); + /** + * Sets the translation of the TaskbarView during the swipe up gesture. + */ + public void setTranslationYForSwipe(float transY) { + mTaskbarIconTranslationYForSwipe = transY; + updateTranslationY(); } /** - * Sets the taskbar icon alignment relative to Launcher hotseat icons + * Sets the translation of the TaskbarView during the spring on stash animation. + */ + public void setTranslationYForStash(float transY) { + mTaskbarIconTranslationYForSpringOnStash = transY; + updateTranslationY(); + } + + private void updateTranslationY() { + mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value + + mTaskbarIconTranslationYForStash.value + + mTaskbarIconTranslationYForSwipe + + mTaskbarIconTranslationYForSpringOnStash); + } + + /** + * Updates the Taskbar's themed icons background according to the progress between in-app/home. + */ + protected void updateIconsBackground() { + mTaskbarView.setThemedIconsBackgroundColor( + ColorUtils.blendARGB( + mLauncherThemedIconsBackgroundColor, + mTaskbarThemedIconsBackgroundColor, + mThemedIconsBackgroundProgress.value + )); + } + + private ValueAnimator createRevealAnimForView(View view, boolean isStashed, float newWidth, + boolean isQsb) { + Rect viewBounds = new Rect(0, 0, view.getWidth(), view.getHeight()); + int centerY = viewBounds.centerY(); + int halfHandleHeight = mStashedHandleHeight / 2; + final int top = centerY - halfHandleHeight; + final int bottom = centerY + halfHandleHeight; + + final int left; + final int right; + // QSB will crop from the 'start' whereas all other icons will crop from the center. + if (isQsb) { + if (mIsRtl) { + right = viewBounds.right; + left = (int) (right - newWidth); + } else { + left = viewBounds.left; + right = (int) (left + newWidth); + } + } else { + int widthDelta = (int) ((viewBounds.width() - newWidth) / 2); + + left = viewBounds.left + widthDelta; + right = viewBounds.right - widthDelta; + } + + Rect stashedRect = new Rect(left, top, right, bottom); + // QSB radius can be > 0 since it does not have any UI elements outside of it bounds. + float radius = isQsb + ? viewBounds.height() / 2f + : 0f; + float stashedRadius = stashedRect.height() / 2f; + + return new RoundedRectRevealOutlineProvider(radius, stashedRadius, viewBounds, stashedRect) + .createRevealAnimator(view, !isStashed, 0); + } + + /** + * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the icon shape + * and size. + * @param as The AnimatorSet to add all animations to. + * @param isStashed When true, the icon crops vertically to the size of the stashed handle. + * When false, the reverse happens. + * @param duration The duration of the animation. + * @param interpolator The interpolator to use for all animations. + */ + public void addRevealAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, + Interpolator interpolator) { + AnimatorSet reveal = new AnimatorSet(); + + Rect stashedBounds = new Rect(); + mControllers.stashedHandleViewController.getStashedHandleBounds(stashedBounds); + + int numIcons = mTaskbarView.getChildCount(); + float newChildWidth = stashedBounds.width() / (float) numIcons; + + // All children move the same y-amount since they will be cropped to the same centerY. + float croppedTransY = mTaskbarView.getIconTouchSize() - stashedBounds.height(); + + for (int i = mTaskbarView.getChildCount() - 1; i >= 0; i--) { + View child = mTaskbarView.getChildAt(i); + boolean isQsb = child == mTaskbarView.getQsb(); + + // Crop the icons to/from the nav handle shape. + reveal.play(createRevealAnimForView(child, isStashed, newChildWidth, isQsb) + .setDuration(duration)); + + // Translate the icons to/from their locations as the "nav handle." + + // All of the Taskbar icons will overlap the entirety of the stashed handle + // And the QSB, if inline, will overlap part of stashed handle as well. + float currentPosition = isQsb ? child.getX() : child.getLeft(); + float newPosition = stashedBounds.left + (newChildWidth * i); + final float croppedTransX; + // We look at 'left' and 'right' values to ensure that the children stay within the + // bounds of the stashed handle since the new width only occurs at the end of the anim. + if (currentPosition > newPosition) { + float newRight = stashedBounds.right - (newChildWidth + * (numIcons - 1 - i)); + croppedTransX = -(currentPosition + child.getWidth() - newRight); + } else { + croppedTransX = newPosition - currentPosition; + } + float[] transX = isStashed + ? new float[] {croppedTransX} + : new float[] {croppedTransX, 0}; + float[] transY = isStashed + ? new float[] {croppedTransY} + : new float[] {croppedTransY, 0}; + + if (child instanceof Reorderable) { + MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); + + reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationX(INDEX_TASKBAR_REVEAL_ANIM), + MULTI_PROPERTY_VALUE, transX) + .setDuration(duration)); + reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationY(INDEX_TASKBAR_REVEAL_ANIM), + MULTI_PROPERTY_VALUE, transY)); + as.addListener(forEndCallback(() -> + mtd.setTranslation(INDEX_TASKBAR_REVEAL_ANIM, 0, 0))); + } else { + reveal.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, transX) + .setDuration(duration)); + reveal.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_Y, transY)); + as.addListener(forEndCallback(() -> { + child.setTranslationX(0); + child.setTranslationY(0); + })); + } + } + + reveal.setInterpolator(interpolator); + as.play(reveal); + } + + /** + * Sets the Taskbar icon alignment relative to Launcher hotseat icons * @param alignmentRatio [0, 1] * 0 => not aligned * 1 => fully aligned */ public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) { - if (mIconAlignControllerLazy == null) { + boolean isHotseatIconOnTopWhenAligned = + mControllers.uiController.isHotseatIconOnTopWhenAligned(); + // When mIsHotseatIconOnTopWhenAligned changes, animation needs to be re-created. + if (mIconAlignControllerLazy == null + || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned) { + mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned; mIconAlignControllerLazy = createIconAlignmentController(launcherDp); } mIconAlignControllerLazy.setPlayFraction(alignmentRatio); @@ -196,37 +423,131 @@ public class TaskbarViewController { } /** - * Creates an animation for aligning the taskbar icons with the provided Launcher device profile + * Creates an animation for aligning the Taskbar icons with the provided Launcher device profile */ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) { mOnControllerPreCreateCallback.run(); PendingAnimation setter = new PendingAnimation(100); + DeviceProfile taskbarDp = mActivity.getDeviceProfile(); Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity); - float scaleUp = ((float) launcherDp.iconSizePx) / mActivity.getDeviceProfile().iconSizePx; - int hotseatCellSize = - (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right) - / launcherDp.numShownHotseatIcons; + float scaleUp = ((float) launcherDp.iconSizePx) / taskbarDp.taskbarIconSize; + int borderSpacing = launcherDp.hotseatBorderSpace; + int hotseatCellSize = DeviceProfile.calculateCellWidth( + launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right, + borderSpacing, + launcherDp.numShownHotseatIcons); + + boolean isToHome = mControllers.uiController.isIconAlignedWithHotseat(); + // If Hotseat is not the top element, Taskbar should maintain in-app state as it fades out, + // or fade in while already in in-app state. + Interpolator interpolator = mIsHotseatIconOnTopWhenAligned ? LINEAR : FINAL_FRAME; int offsetY = launcherDp.getTaskbarOffsetY(); - setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR); - setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, LINEAR); + setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, interpolator); + setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, interpolator); + setter.setFloat(mTaskbarNavButtonTranslationYForInAppDisplay, VALUE, offsetY, interpolator); + + if (Utilities.isDarkTheme(mTaskbarView.getContext())) { + setter.addFloat(mThemedIconsBackgroundProgress, VALUE, 1f, 0f, LINEAR); + } int collapsedHeight = mActivity.getDefaultTaskbarWindowHeight(); - int expandedHeight = Math.max(collapsedHeight, - mActivity.getDeviceProfile().taskbarSize + offsetY); + int expandedHeight = Math.max(collapsedHeight, taskbarDp.taskbarHeight + offsetY); setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight( anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight)); - int count = mTaskbarView.getChildCount(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < mTaskbarView.getChildCount(); i++) { View child = mTaskbarView.getChildAt(i); - ItemInfo info = (ItemInfo) child.getTag(); - setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR); + boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView(); + 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())) { + if (!isToHome + && mIsHotseatIconOnTopWhenAligned + && mControllers.taskbarStashController.isStashed()) { + // Prevent All Apps icon from appearing when going from hotseat to nav handle. + setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0f, 0f)); + } else { + setter.setViewAlpha(child, 0, + isToHome + ? Interpolators.clampToProgress(LINEAR, 0f, 0.17f) + : Interpolators.clampToProgress(LINEAR, 0.72f, 0.84f)); + } + } - float childCenter = (child.getLeft() + child.getRight()) / 2; - float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId - + hotseatCellSize / 2; - setter.setFloat(child, ICON_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR); + if (child == mTaskbarView.getQsb()) { + boolean isRtl = Utilities.isRtl(child.getResources()); + float hotseatIconCenter = isRtl + ? launcherDp.widthPx - hotseatPadding.right + borderSpacing + + launcherDp.hotseatQsbWidth / 2f + : hotseatPadding.left - borderSpacing - launcherDp.hotseatQsbWidth / 2f; + float childCenter = (child.getLeft() + child.getRight()) / 2f; + float halfQsbIconWidthDiff = + (launcherDp.hotseatQsbWidth - taskbarDp.taskbarIconSize) / 2f; + float scale = ((float) taskbarDp.taskbarIconSize) + / launcherDp.hotseatQsbVisualHeight; + setter.addFloat(child, SCALE_PROPERTY, scale, 1f, interpolator); + + float fromX = isRtl ? -halfQsbIconWidthDiff : halfQsbIconWidthDiff; + float toX = hotseatIconCenter - childCenter; + if (child instanceof Reorderable) { + MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); + + setter.addFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM), + MULTI_PROPERTY_VALUE, fromX, toX, interpolator); + setter.setFloat(mtd.getTranslationY(INDEX_TASKBAR_ALIGNMENT_ANIM), + MULTI_PROPERTY_VALUE, mTaskbarBottomMargin, interpolator); + } else { + setter.addFloat(child, VIEW_TRANSLATE_X, fromX, toX, interpolator); + setter.setFloat(child, VIEW_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); + } + + if (mIsHotseatIconOnTopWhenAligned) { + setter.addFloat(child, VIEW_ALPHA, 0f, 1f, + isToHome + ? Interpolators.clampToProgress(LINEAR, 0f, 0.35f) + : mActivity.getDeviceProfile().isQsbInline + ? Interpolators.clampToProgress(LINEAR, 0f, 1f) + : Interpolators.clampToProgress(LINEAR, 0.84f, 1f)); + } + setter.addOnFrameListener(animator -> AlphaUpdateListener.updateVisibility(child)); + continue; + } + + int 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 (child.getTag() instanceof ItemInfo) { + positionInHotseat = ((ItemInfo) child.getTag()).screenId; + } else { + Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); + continue; + } + + float hotseatIconCenter = hotseatPadding.left + + (hotseatCellSize + borderSpacing) * positionInHotseat + + hotseatCellSize / 2f; + float childCenter = (child.getLeft() + child.getRight()) / 2f; + float toX = hotseatIconCenter - childCenter; + if (child instanceof Reorderable) { + MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); + + setter.setFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM), + MULTI_PROPERTY_VALUE, toX, interpolator); + setter.setFloat(mtd.getTranslationY(INDEX_TASKBAR_ALIGNMENT_ANIM), + MULTI_PROPERTY_VALUE, mTaskbarBottomMargin, interpolator); + } else { + setter.setFloat(child, VIEW_TRANSLATE_X, toX, interpolator); + setter.setFloat(child, VIEW_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); + } + setter.setFloat(child, SCALE_PROPERTY, scaleUp, interpolator); } AnimatorPlaybackController controller = setter.createPlaybackController(); @@ -235,13 +556,33 @@ public class TaskbarViewController { } public void onRotationChanged(DeviceProfile deviceProfile) { - if (areIconsVisible()) { - // We only translate on rotation when on home + if (!mControllers.uiController.isIconAlignedWithHotseat()) { + // We only translate on rotation when icon is aligned with hotseat return; } + mActivity.setTaskbarWindowHeight( + deviceProfile.taskbarHeight + deviceProfile.getTaskbarOffsetY()); mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY()); } + /** + * Maps the given operator to all the top-level children of TaskbarView. + */ + public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { + mTaskbarView.mapOverItems(op); + } + + /** + * Returns the first icon to match the given parameter, in priority from: + * 1) Icons directly on Taskbar + * 2) FolderIcon of the Folder containing the given icon + * 3) All Apps button + */ + public View getFirstIconMatch(Predicate matcher) { + Predicate folderMatcher = ItemInfoMatcher.forFolderMatch(matcher); + return mTaskbarView.getFirstMatch(matcher, folderMatcher); + } + /** * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's * touch bounds. @@ -250,6 +591,36 @@ public class TaskbarViewController { return mTaskbarView.isEventOverAnyItem(ev); } + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarViewController:"); + + mTaskbarIconAlpha.dump( + prefix + "\t", + pw, + "mTaskbarIconAlpha", + "ALPHA_INDEX_HOME", + "ALPHA_INDEX_KEYGUARD", + "ALPHA_INDEX_STASH", + "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(); + } + + /** Call TaskbarModelCallbacks to update running apps. */ + public void updateRunningApps() { + mModelCallbacks.updateRunningApps(); + } + /** * Callbacks for {@link TaskbarView} to interact with its controller. */ @@ -263,6 +634,13 @@ public class TaskbarViewController { return mActivity.getItemOnClickListener(); } + public View.OnClickListener getAllAppsButtonClickListener() { + return v -> { + mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP); + mControllers.taskbarAllAppsController.show(); + }; + } + public View.OnLongClickListener getIconOnLongClickListener() { return mControllers.taskbarDragController::startDragOnLongClick; } @@ -275,6 +653,7 @@ public class TaskbarViewController { /** * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to * consume the touch so TaskbarView treats it as an ACTION_CANCEL. + * TODO(b/270395798): We can remove this entirely once we remove the Transient Taskbar flag. */ public boolean onTouchEvent(MotionEvent motionEvent) { final float x = motionEvent.getRawX(); @@ -303,33 +682,15 @@ public class TaskbarViewController { } break; } + return false; } + + /** + * Notifies launcher to update icon alignment. + */ + public void notifyIconLayoutBoundsChanged() { + mControllers.uiController.onIconLayoutBoundsChanged(); + } } - - public static final FloatProperty ICON_TRANSLATE_X = - new FloatProperty("taskbarAligmentTranslateX") { - - @Override - public void setValue(View view, float v) { - if (view instanceof BubbleTextView) { - ((BubbleTextView) view).setTranslationXForTaskbarAlignmentAnimation(v); - } else if (view instanceof FolderIcon) { - ((FolderIcon) view).setTranslationForTaskbarAlignmentAnimation(v); - } else { - view.setTranslationX(v); - } - } - - @Override - public Float get(View view) { - if (view instanceof BubbleTextView) { - return ((BubbleTextView) view) - .getTranslationXForTaskbarAlignmentAnimation(); - } else if (view instanceof FolderIcon) { - return ((FolderIcon) view).getTranslationXForTaskbarAlignmentAnimation(); - } - return view.getTranslationX(); - } - }; } diff --git a/quickstep/src/com/android/launcher3/taskbar/Utilities.java b/quickstep/src/com/android/launcher3/taskbar/Utilities.java new file mode 100644 index 0000000000..47d6684727 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/Utilities.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Various utilities shared amongst the Taskbar's classes. + */ +public final class Utilities { + + private Utilities() {} + + /** + * Sets drag, long-click, and split selection behavior on 1P and 3P launchers with Taskbar + */ + static void setOverviewDragState(TaskbarControllers controllers, + boolean disallowGlobalDrag, boolean disallowLongClick, + boolean allowInitialSplitSelection) { + controllers.taskbarDragController.setDisallowGlobalDrag(disallowGlobalDrag); + controllers.taskbarDragController.setDisallowLongClick(disallowLongClick); + controllers.taskbarAllAppsController.setDisallowGlobalDrag(disallowGlobalDrag); + controllers.taskbarAllAppsController.setDisallowLongClick(disallowLongClick); + controllers.taskbarPopupController.setAllowInitialSplitSelection( + allowInitialSplitSelection); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt new file mode 100644 index 0000000000..5a5ff8e880 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import android.animation.AnimatorSet +import android.graphics.Canvas +import android.view.View +import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY +import com.android.launcher3.util.DisplayController +import com.android.launcher3.views.BaseDragLayer +import com.android.systemui.animation.ViewRootSync +import java.io.PrintWriter + +private const val TASKBAR_ICONS_FADE_DURATION = 300L +private const val STASHED_HANDLE_FADE_DURATION = 180L +private const val TEMP_BACKGROUND_WINDOW_TITLE = "VoiceInteractionTaskbarBackground" + +/** + * Controls Taskbar behavior while Voice Interaction Window (assistant) is showing. Specifically: + * - We always hide the taskbar icons or stashed handle, whichever is currently showing. + * - For persistent taskbar, we also move the taskbar background to a new window/layer + * (TYPE_APPLICATION_OVERLAY) which is behind the assistant. + * - For transient taskbar, we hide the real taskbar background (if it's showing). + */ +class VoiceInteractionWindowController(val context: TaskbarActivityContext) : + TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController { + + private val isSeparateBackgroundEnabled = !DisplayController.isTransientTaskbar(context) + private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context) + private val nonTouchableInsetsComputer = + ViewTreeObserver.OnComputeInternalInsetsListener { + it.touchableRegion.setEmpty() + it.setTouchableInsets(TOUCHABLE_INSETS_REGION) + } + + // Initialized in init. + private lateinit var controllers: TaskbarControllers + // Only initialized if isSeparateBackgroundEnabled + private var separateWindowForTaskbarBackground: BaseDragLayer? = null + private var separateWindowLayoutParams: WindowManager.LayoutParams? = null + + private var isVoiceInteractionWindowVisible: Boolean = false + private var pendingAttachedToWindowListener: View.OnAttachStateChangeListener? = null + + fun init(controllers: TaskbarControllers) { + this.controllers = controllers + + if (!isSeparateBackgroundEnabled) { + return + } + + separateWindowForTaskbarBackground = + object : BaseDragLayer(context, null, 0) { + override fun recreateControllers() { + mControllers = emptyArray() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + if (controllers.taskbarStashController.isTaskbarVisibleAndNotStashing) { + taskbarBackgroundRenderer.draw(canvas) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + viewTreeObserver.addOnComputeInternalInsetsListener(nonTouchableInsetsComputer) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + viewTreeObserver.removeOnComputeInternalInsetsListener( + nonTouchableInsetsComputer + ) + } + } + separateWindowForTaskbarBackground?.recreateControllers() + separateWindowForTaskbarBackground?.setWillNotDraw(false) + + separateWindowLayoutParams = + context.createDefaultWindowLayoutParams( + TYPE_APPLICATION_OVERLAY, + TEMP_BACKGROUND_WINDOW_TITLE + ) + separateWindowLayoutParams?.isSystemApplicationOverlay = true + } + + fun onDestroy() { + setIsVoiceInteractionWindowVisible(visible = false, skipAnim = true) + separateWindowForTaskbarBackground?.removeOnAttachStateChangeListener( + pendingAttachedToWindowListener + ) + } + + fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) { + if (isVoiceInteractionWindowVisible == visible) { + return + } + isVoiceInteractionWindowVisible = visible + + // Fade out taskbar icons and stashed handle. + val taskbarIconAlpha = if (isVoiceInteractionWindowVisible) 0f else 1f + val fadeTaskbarIcons = + controllers.taskbarViewController.taskbarIconAlpha + .get(TaskbarViewController.ALPHA_INDEX_ASSISTANT_INVOKED) + .animateToValue(taskbarIconAlpha) + .setDuration(TASKBAR_ICONS_FADE_DURATION) + val fadeStashedHandle = + controllers.stashedHandleViewController.stashedHandleAlpha + .get(StashedHandleViewController.ALPHA_INDEX_ASSISTANT_INVOKED) + .animateToValue(taskbarIconAlpha) + .setDuration(STASHED_HANDLE_FADE_DURATION) + val animSet = AnimatorSet() + animSet.play(fadeTaskbarIcons) + animSet.play(fadeStashedHandle) + if (!isSeparateBackgroundEnabled) { + val fadeTaskbarBackground = + controllers.taskbarDragLayerController.assistantBgTaskbar + .animateToValue(taskbarIconAlpha) + .setDuration(TASKBAR_ICONS_FADE_DURATION) + animSet.play(fadeTaskbarBackground) + } + animSet.start() + if (skipAnim) { + animSet.end() + } + + if (isSeparateBackgroundEnabled) { + moveTaskbarBackgroundToAppropriateLayer(skipAnim) + } + } + + /** + * Either: + * + * Hides the TaskbarDragLayer background and creates a new window to draw just that background. + * + * OR + * + * Removes the temporary window and show the TaskbarDragLayer background again. + */ + private fun moveTaskbarBackgroundToAppropriateLayer(skipAnim: Boolean) { + val moveToLowerLayer = isVoiceInteractionWindowVisible + val onWindowsSynchronized = + if (moveToLowerLayer) { + // First add the temporary window, then hide the overlapping taskbar background. + context.addWindowView( + separateWindowForTaskbarBackground, + separateWindowLayoutParams + ); + { controllers.taskbarDragLayerController.setIsBackgroundDrawnElsewhere(true) } + } else { + // First reapply the original taskbar background, then remove the temporary window. + controllers.taskbarDragLayerController.setIsBackgroundDrawnElsewhere(false); + { context.removeWindowView(separateWindowForTaskbarBackground) } + } + + if (skipAnim) { + onWindowsSynchronized() + } else { + separateWindowForTaskbarBackground?.runWhenAttachedToWindow { + ViewRootSync.synchronizeNextDraw( + separateWindowForTaskbarBackground!!, + context.dragLayer, + onWindowsSynchronized + ) + } + } + } + + private fun View.runWhenAttachedToWindow(onAttachedToWindow: () -> Unit) { + if (isAttachedToWindow) { + onAttachedToWindow() + return + } + removeOnAttachStateChangeListener(pendingAttachedToWindowListener) + pendingAttachedToWindowListener = + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + onAttachedToWindow() + removeOnAttachStateChangeListener(this) + pendingAttachedToWindowListener = null + } + + override fun onViewDetachedFromWindow(v: View) {} + } + addOnAttachStateChangeListener(pendingAttachedToWindowListener) + } + + override fun setCornerRoundness(cornerRoundness: Float) { + if (!isSeparateBackgroundEnabled) { + return + } + taskbarBackgroundRenderer.setCornerRoundness(cornerRoundness) + separateWindowForTaskbarBackground?.invalidate() + } + + override fun dumpLogs(prefix: String, pw: PrintWriter) { + pw.println(prefix + "VoiceInteractionWindowController:") + pw.println("$prefix\tisSeparateBackgroundEnabled=$isSeparateBackgroundEnabled") + pw.println("$prefix\tisVoiceInteractionWindowVisible=$isVoiceInteractionWindowVisible") + pw.println( + "$prefix\tisSeparateTaskbarBackgroundAttachedToWindow=" + + "${separateWindowForTaskbarBackground?.isAttachedToWindow}" + ) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java new file mode 100644 index 0000000000..eeca329e80 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java @@ -0,0 +1,76 @@ +/* + * 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 android.view.WindowInsets; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; + +/** All apps container accessible from taskbar. */ +public class TaskbarAllAppsContainerView extends + ActivityAllAppsContainerView { + + public TaskbarAllAppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskbarAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + setInsets(insets.getInsets(WindowInsets.Type.systemBars()).toRect()); + return super.onApplyWindowInsets(insets); + } + + @Override + protected View inflateSearchBox() { + // 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 + protected boolean isSearchSupported() { + return false; + } + + @Override + protected void updateBackground(DeviceProfile deviceProfile) { + super.updateBackground(deviceProfile); + // TODO(b/240670050): Remove this and add header protection for the taskbar entrypoint. + mBottomSheetBackground.setBackgroundResource(R.drawable.bg_rounded_corner_bottom_sheet); + } + + @Override + public boolean isInAllApps() { + // All apps is always open + return true; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java new file mode 100644 index 0000000000..4a95a8f718 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java @@ -0,0 +1,144 @@ +/* + * 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 androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.R; +import com.android.launcher3.appprediction.PredictionRowView; +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; + +import java.util.List; + +/** + * Handles the all apps overlay window initialization, updates, and its data. + *

+ * All apps is in an application overlay window instead of taskbar's navigation bar panel window, + * because a navigation bar panel is higher than UI components that all apps should be below such as + * the notification tray. + *

+ * The all apps window is created and destroyed upon opening and closing all apps, respectively. + * Application data may be bound while the window does not exist, so this controller will store + * the models for the next all apps session. + */ +public final class TaskbarAllAppsController { + + private TaskbarControllers mControllers; + private @Nullable TaskbarAllAppsSlideInView mSlideInView; + private @Nullable TaskbarAllAppsContainerView mAppsView; + + // Application data models. + private AppInfo[] mApps; + private int mAppsModelFlags; + private List mPredictedApps; + private boolean mDisallowGlobalDrag; + private boolean mDisallowLongClick; + + /** Initialize the controller. */ + public void init(TaskbarControllers controllers, boolean allAppsVisible) { + mControllers = controllers; + + /* + * Recreate All Apps if it was open in the previous Taskbar instance (e.g. the configuration + * changed). + */ + if (allAppsVisible) { + show(false); + } + } + + /** Updates the current {@link AppInfo} instances. */ + public void setApps(AppInfo[] apps, int flags) { + mApps = apps; + mAppsModelFlags = flags; + if (mAppsView != null) { + mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags); + } + } + + public void setDisallowGlobalDrag(boolean disableDragForOverviewState) { + mDisallowGlobalDrag = disableDragForOverviewState; + } + + public void setDisallowLongClick(boolean disallowLongClick) { + mDisallowLongClick = disallowLongClick; + } + + /** Updates the current predictions. */ + public void setPredictedApps(List predictedApps) { + mPredictedApps = predictedApps; + if (mAppsView != null) { + mAppsView.getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class) + .setPredictedApps(mPredictedApps); + } + } + + /** Opens the {@link TaskbarAllAppsContainerView} in a new window. */ + public void show() { + show(true); + } + + /** Returns {@code true} if All Apps is open. */ + public boolean isOpen() { + return mSlideInView != null && mSlideInView.isOpen(); + } + + private void show(boolean animate) { + if (mAppsView != null) { + return; + } + // mControllers and getSharedState should never be null here. Do not handle null-pointer + // to catch invalid states. + mControllers.getSharedState().allAppsVisible = true; + + TaskbarOverlayContext overlayContext = + mControllers.taskbarOverlayController.requestWindow(); + mSlideInView = (TaskbarAllAppsSlideInView) overlayContext.getLayoutInflater().inflate( + R.layout.taskbar_all_apps, overlayContext.getDragLayer(), false); + mSlideInView.addOnCloseListener(() -> { + mControllers.getSharedState().allAppsVisible = false; + mSlideInView = null; + mAppsView = null; + }); + TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController( + overlayContext, mSlideInView, mControllers); + + viewController.show(animate); + mAppsView = overlayContext.getAppsView(); + mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags); + mAppsView.getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class) + .setPredictedApps(mPredictedApps); + // 1 alternative that would be more work: + // Create a shared drag layer between taskbar and taskbarAllApps so that when dragging + // starts and taskbarAllApps can close, but the drag layer that the view is being dragged in + // doesn't also close + overlayContext.getDragController().setDisallowGlobalDrag(mDisallowGlobalDrag); + overlayContext.getDragController().setDisallowLongClick(mDisallowLongClick); + } + + + @VisibleForTesting + public int getTaskbarAllAppsTopPadding() { + // Allow null-pointer since this should only be null if the apps view is not showing. + return mAppsView.getActiveRecyclerView().getClipBounds().top; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java new file mode 100644 index 0000000000..53fe06d32c --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java @@ -0,0 +1,54 @@ +/* + * 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 new file mode 100644 index 0000000000..c1597b6507 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java @@ -0,0 +1,159 @@ +/* + * 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 static com.android.launcher3.anim.Interpolators.EMPHASIZED; + +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.animation.Interpolator; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Insettable; +import com.android.launcher3.R; +import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.launcher3.views.AbstractSlideInView; + +/** Wrapper for taskbar all apps with slide-in behavior. */ +public class TaskbarAllAppsSlideInView extends AbstractSlideInView + implements Insettable, DeviceProfile.OnDeviceProfileChangeListener { + private TaskbarAllAppsContainerView mAppsView; + private float mShiftRange; + + // Initialized in init. + private TaskbarAllAppsCallbacks mAllAppsCallbacks; + + public TaskbarAllAppsSlideInView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskbarAllAppsSlideInView(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + void init(TaskbarAllAppsCallbacks callbacks) { + mAllAppsCallbacks = callbacks; + } + + /** Opens the all apps view. */ + void show(boolean animate) { + if (mIsOpen || mOpenCloseAnimator.isRunning()) { + return; + } + mIsOpen = true; + attachToContainer(); + + if (animate) { + mOpenCloseAnimator.setValues( + PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); + mOpenCloseAnimator.setInterpolator(EMPHASIZED); + mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start(); + } else { + mTranslationShift = TRANSLATION_SHIFT_OPENED; + } + } + + /** The apps container inside this view. */ + TaskbarAllAppsContainerView getAppsView() { + return mAppsView; + } + + @Override + protected void handleClose(boolean animate) { + handleClose(animate, mAllAppsCallbacks.getCloseDuration()); + } + + @Override + protected Interpolator getIdleInterpolator() { + return EMPHASIZED; + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_TASKBAR_ALL_APPS) != 0; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mAppsView = findViewById(R.id.apps_view); + mContent = mAppsView; + + DeviceProfile dp = mActivityContext.getDeviceProfile(); + setShiftRange(dp.allAppsShiftRange); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mActivityContext.addOnDeviceProfileChangeListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mActivityContext.removeOnDeviceProfileChangeListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + setTranslationShift(mTranslationShift); + } + + @Override + protected int getScrimColor(Context context) { + return context.getColor(R.color.widgets_picker_scrim); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !mAppsView.shouldContainerScroll(ev); + } + return super.onControllerInterceptTouchEvent(ev); + } + + @Override + public void setInsets(Rect insets) { + mAppsView.setInsets(insets); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + setShiftRange(dp.allAppsShiftRange); + setTranslationShift(TRANSLATION_SHIFT_OPENED); + } + + private void setShiftRange(float shiftRange) { + mShiftRange = shiftRange; + } + + @Override + protected float getShiftRange() { + return mShiftRange; + } + + @Override + protected boolean isEventOverContent(MotionEvent ev) { + return getPopupContainer().isEventOverView(mAppsView.getVisibleContainerView(), ev); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java new file mode 100644 index 0000000000..7a3b3e8c7b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java @@ -0,0 +1,121 @@ +/* + * 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 static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_TASKBAR_ALL_APPS; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.appprediction.AppsDividerView; +import com.android.launcher3.appprediction.PredictionRowView; +import com.android.launcher3.taskbar.NavbarButtonsViewController; +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.TaskbarStashController; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; +import com.android.launcher3.util.DisplayController; + +/** + * Handles the {@link TaskbarAllAppsContainerView} behavior and synchronizes its transitions with + * taskbar stashing. + */ +final class TaskbarAllAppsViewController { + + private final TaskbarOverlayContext mContext; + private final TaskbarAllAppsSlideInView mSlideInView; + private final TaskbarAllAppsContainerView mAppsView; + private final TaskbarStashController mTaskbarStashController; + private final NavbarButtonsViewController mNavbarButtonsViewController; + private final TaskbarOverlayController mOverlayController; + + TaskbarAllAppsViewController( + TaskbarOverlayContext context, + TaskbarAllAppsSlideInView slideInView, + TaskbarControllers taskbarControllers) { + + mContext = context; + mSlideInView = slideInView; + mAppsView = mSlideInView.getAppsView(); + mTaskbarStashController = taskbarControllers.taskbarStashController; + mNavbarButtonsViewController = taskbarControllers.navbarButtonsViewController; + mOverlayController = taskbarControllers.taskbarOverlayController; + + mSlideInView.init(new TaskbarAllAppsCallbacks()); + setUpIconLongClick(); + setUpAppDivider(); + setUpTaskbarStashing(); + } + + /** Starts the {@link TaskbarAllAppsSlideInView} enter transition. */ + void show(boolean animate) { + mSlideInView.show(animate); + } + + /** Closes the {@link TaskbarAllAppsSlideInView}. */ + void close(boolean animate) { + mSlideInView.close(animate); + } + + private void setUpIconLongClick() { + mAppsView.setOnIconLongClickListener( + mContext.getDragController()::startDragOnLongClick); + mAppsView.getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class) + .setOnIconLongClickListener( + mContext.getDragController()::startDragOnLongClick); + } + + private void setUpAppDivider() { + mAppsView.getFloatingHeaderView() + .findFixedRowByType(AppsDividerView.class) + .setShowAllAppsLabel(!mContext.getOnboardingPrefs().hasReachedMaxCount( + ALL_APPS_VISITED_COUNT)); + mContext.getOnboardingPrefs().incrementEventCount(ALL_APPS_VISITED_COUNT); + } + + private void setUpTaskbarStashing() { + mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, true); + mTaskbarStashController.applyState(mOverlayController.getOpenDuration()); + + mNavbarButtonsViewController.setSlideInViewVisible(true); + mSlideInView.setOnCloseBeginListener(() -> { + mNavbarButtonsViewController.setSlideInViewVisible(false); + AbstractFloatingView.closeOpenContainer( + mContext, AbstractFloatingView.TYPE_ACTION_POPUP); + + if (DisplayController.isTransientTaskbar(mContext)) { + mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false); + mTaskbarStashController.applyState(mOverlayController.getCloseDuration()); + } else { + // Post in case view is closing due to gesture navigation. If a gesture is in + // progress, wait to unstash until after the gesture is finished. + MAIN_EXECUTOR.post(() -> mTaskbarStashController.resetFlagIfNoGestureInProgress( + FLAG_STASHED_IN_TASKBAR_ALL_APPS)); + } + }); + } + + class TaskbarAllAppsCallbacks { + int getOpenDuration() { + return mOverlayController.getOpenDuration(); + } + + int getCloseDuration() { + return mOverlayController.getCloseDuration(); + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt new file mode 100644 index 0000000000..e704e5116b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt @@ -0,0 +1,46 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import com.android.launcher3.R +import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter + +/** + * Meant to be a simple container for data subclasses will need + * + * Assumes that the 3 navigation buttons (back/home/recents) have already been added to + * [navButtonContainer] + * + * @property navButtonContainer ViewGroup that holds the 3 navigation buttons. + * @property endContextualContainer ViewGroup that holds the end contextual button (ex, IME + * dismiss). + * @property startContextualContainer ViewGroup that holds the start contextual button (ex, A11y). + */ +abstract class AbstractNavButtonLayoutter( + val resources: Resources, + val navButtonContainer: LinearLayout, + protected val endContextualContainer: ViewGroup, + protected val startContextualContainer: ViewGroup +) : NavButtonLayoutter { + protected val homeButton: ImageView = navButtonContainer.requireViewById(R.id.home) + protected val recentsButton: ImageView = navButtonContainer.requireViewById(R.id.recent_apps) + protected val backButton: ImageView = navButtonContainer.requireViewById(R.id.back) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt new file mode 100644 index 0000000000..c093c9240a --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt @@ -0,0 +1,100 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.PaintDrawable +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import com.android.launcher3.DeviceProfile +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_BACK_BUTTON_LEFT_MARGIN_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_HOME_BUTTON_LEFT_MARGIN_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_ICON_SIZE_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_NAV_BUTTONS_CORNER_RADIUS_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_NAV_BUTTONS_HEIGHT_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DIMEN_TASKBAR_NAV_BUTTONS_WIDTH_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DRAWABLE_SYSBAR_BACK_KIDS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.DRAWABLE_SYSBAR_HOME_KIDS + +class KidsNavLayoutter( + resources: Resources, + navBarContainer: LinearLayout, + endContextualContainer: ViewGroup, + startContextualContainer: ViewGroup +) : + AbstractNavButtonLayoutter( + resources, + navBarContainer, + endContextualContainer, + startContextualContainer + ) { + + override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) { + val iconSize: Int = resources.getDimensionPixelSize(DIMEN_TASKBAR_ICON_SIZE_KIDS) + val buttonWidth: Int = resources.getDimensionPixelSize(DIMEN_TASKBAR_NAV_BUTTONS_WIDTH_KIDS) + val buttonHeight: Int = + resources.getDimensionPixelSize(DIMEN_TASKBAR_NAV_BUTTONS_HEIGHT_KIDS) + val buttonRadius: Int = + resources.getDimensionPixelSize(DIMEN_TASKBAR_NAV_BUTTONS_CORNER_RADIUS_KIDS) + val paddingLeft = (buttonWidth - iconSize) / 2 + val paddingTop = (buttonHeight - iconSize) / 2 + + // Update icons + backButton.setImageDrawable(backButton.context.getDrawable(DRAWABLE_SYSBAR_BACK_KIDS)) + backButton.scaleType = ImageView.ScaleType.FIT_CENTER + backButton.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop) + homeButton.setImageDrawable(homeButton.getContext().getDrawable(DRAWABLE_SYSBAR_HOME_KIDS)) + homeButton.scaleType = ImageView.ScaleType.FIT_CENTER + homeButton.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop) + + // Home button layout + val homeLayoutparams = LinearLayout.LayoutParams(buttonWidth, buttonHeight) + val homeButtonLeftMargin: Int = + resources.getDimensionPixelSize(DIMEN_TASKBAR_HOME_BUTTON_LEFT_MARGIN_KIDS) + homeLayoutparams.setMargins(homeButtonLeftMargin, 0, 0, 0) + homeButton.layoutParams = homeLayoutparams + + // Back button layout + val backLayoutParams = LinearLayout.LayoutParams(buttonWidth, buttonHeight) + val backButtonLeftMargin: Int = + resources.getDimensionPixelSize(DIMEN_TASKBAR_BACK_BUTTON_LEFT_MARGIN_KIDS) + backLayoutParams.setMargins(backButtonLeftMargin, 0, 0, 0) + backButton.layoutParams = backLayoutParams + + // Button backgrounds + val whiteWith10PctAlpha = Color.argb(0.1f, 1f, 1f, 1f) + val buttonBackground = PaintDrawable(whiteWith10PctAlpha) + buttonBackground.setCornerRadius(buttonRadius.toFloat()) + homeButton.background = buttonBackground + backButton.background = buttonBackground + + // Update alignment within taskbar + val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams + navButtonsLayoutParams.apply { + marginStart = navButtonsLayoutParams.marginEnd / 2 + marginEnd = navButtonsLayoutParams.marginStart + gravity = Gravity.CENTER + } + navButtonContainer.requestLayout() + + homeButton.onLongClickListener = null + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/LayoutResourceHelper.java b/quickstep/src/com/android/launcher3/taskbar/navbutton/LayoutResourceHelper.java new file mode 100644 index 0000000000..0d9b855c91 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/LayoutResourceHelper.java @@ -0,0 +1,68 @@ +/* + * 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.navbutton; + +import android.annotation.DimenRes; +import android.annotation.DrawableRes; +import android.annotation.IdRes; + +import com.android.launcher3.R; + +/** + * A class for retrieving resources in Kotlin. + * + * This class should be removed once the build system supports resources loading in Kotlin. + */ +public final class LayoutResourceHelper { + + // -------------------------- + // Kids Nav Layout + @DimenRes + public static final int DIMEN_TASKBAR_ICON_SIZE_KIDS = R.dimen.taskbar_icon_size_kids; + @DrawableRes + public static final int DRAWABLE_SYSBAR_BACK_KIDS = R.drawable.ic_sysbar_back_kids; + @DrawableRes + public static final int DRAWABLE_SYSBAR_HOME_KIDS = R.drawable.ic_sysbar_home_kids; + @DimenRes + public static final int DIMEN_TASKBAR_HOME_BUTTON_LEFT_MARGIN_KIDS = + R.dimen.taskbar_home_button_left_margin_kids; + @DimenRes + public static final int DIMEN_TASKBAR_BACK_BUTTON_LEFT_MARGIN_KIDS = + R.dimen.taskbar_back_button_left_margin_kids; + @DimenRes + public static final int DIMEN_TASKBAR_NAV_BUTTONS_WIDTH_KIDS = + R.dimen.taskbar_nav_buttons_width_kids; + @DimenRes + public static final int DIMEN_TASKBAR_NAV_BUTTONS_HEIGHT_KIDS = + R.dimen.taskbar_nav_buttons_height_kids; + @DimenRes + public static final int DIMEN_TASKBAR_NAV_BUTTONS_CORNER_RADIUS_KIDS = + R.dimen.taskbar_nav_buttons_corner_radius_kids; + + // -------------------------- + // Nav Layout Factory + @IdRes + public static final int ID_START_CONTEXTUAL_BUTTONS = R.id.start_contextual_buttons; + @IdRes + public static final int ID_END_CONTEXTUAL_BUTTONS = R.id.end_contextual_buttons; + @IdRes + public static final int ID_END_NAV_BUTTONS = R.id.end_nav_buttons; + + private LayoutResourceHelper() { + + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt new file mode 100644 index 0000000000..b730b215cd --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt @@ -0,0 +1,124 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.android.launcher3.DeviceProfile +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS +import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.Companion +import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter + +/** + * Select the correct layout for nav buttons + * + * Since layouts are done dynamically for the nav buttons on Taskbar, this class returns a + * corresponding [NavButtonLayoutter] via [Companion.getUiLayoutter] that can help position the + * buttons based on the current [DeviceProfile] + */ +class NavButtonLayoutFactory { + companion object { + /** + * Get the correct instance of [NavButtonLayoutter] + * + * No layouts supported for configurations where: + * * taskbar isn't showing AND + * * the device is not in [phoneMode] OR + * * phone is showing + * * device is using gesture navigation + * + * @param navButtonsView ViewGroup that contains start, end, nav button ViewGroups + * @param isKidsMode no-op when taskbar is hidden/not showing + * @param isInSetup no-op when taskbar is hidden/not showing + * @param phoneMode refers to the device using the taskbar window on phones + * @param isThreeButtonNav are no-ops when taskbar is present/showing + */ + fun getUiLayoutter( + deviceProfile: DeviceProfile, + navButtonsView: FrameLayout, + resources: Resources, + isKidsMode: Boolean, + isInSetup: Boolean, + isThreeButtonNav: Boolean, + phoneMode: Boolean + ): NavButtonLayoutter { + val navButtonContainer = + navButtonsView.requireViewById(ID_END_NAV_BUTTONS) + val endContextualContainer = + navButtonsView.requireViewById(ID_END_CONTEXTUAL_BUTTONS) + val startContextualContainer = + navButtonsView.requireViewById(ID_START_CONTEXTUAL_BUTTONS) + val isPhoneNavMode = phoneMode && isThreeButtonNav + return when { + isPhoneNavMode -> { + if (!deviceProfile.isLandscape) { + PhonePortraitNavLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) + } else { + PhoneLandscapeNavLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) + } + } + deviceProfile.isTaskbarPresent -> { + return when { + isInSetup -> { + SetupNavLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) + } + isKidsMode -> { + KidsNavLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) + } + else -> + TaskbarNavLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) + } + } + else -> error("No layoutter found") + } + } + } + + /** Lays out and provides access to the home, recents, and back buttons for various mischief */ + interface NavButtonLayoutter { + fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt new file mode 100644 index 0000000000..201895fc67 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt @@ -0,0 +1,89 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.core.view.children +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.util.DimensionUtils + +class PhoneLandscapeNavLayoutter( + resources: Resources, + navBarContainer: LinearLayout, + endContextualContainer: ViewGroup, + startContextualContainer: ViewGroup +) : + AbstractNavButtonLayoutter( + resources, + navBarContainer, + endContextualContainer, + startContextualContainer + ) { + + override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) { + // TODO(b/230395757): Polish pending, this is just to make it usable + val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams + val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) + val taskbarDimensions = + DimensionUtils.getTaskbarPhoneDimensions(dp, resources, TaskbarManager.isPhoneMode(dp)) + navButtonContainer.removeAllViews() + navButtonContainer.orientation = LinearLayout.VERTICAL + + navContainerParams.apply { + width = taskbarDimensions.x + height = ViewGroup.LayoutParams.MATCH_PARENT + gravity = Gravity.CENTER + topMargin = endStartMargins + bottomMargin = endStartMargins + marginEnd = 0 + marginStart = 0 + } + + // Swap recents and back button + navButtonContainer.addView(recentsButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(backButton) + + navButtonContainer.layoutParams = navContainerParams + + // Add the spaces in between the nav buttons + val spaceInBetween: Int = + resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween_phone) + navButtonContainer.children.forEachIndexed { i, navButton -> + val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams + buttonLayoutParams.weight = 1f + when (i) { + 0 -> { + buttonLayoutParams.bottomMargin = spaceInBetween / 2 + } + navButtonContainer.childCount - 1 -> { + buttonLayoutParams.topMargin = spaceInBetween / 2 + } + else -> { + buttonLayoutParams.bottomMargin = spaceInBetween / 2 + buttonLayoutParams.topMargin = spaceInBetween / 2 + } + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt new file mode 100644 index 0000000000..f7ac9745f0 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt @@ -0,0 +1,90 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.util.DimensionUtils + +class PhonePortraitNavLayoutter( + resources: Resources, + navBarContainer: LinearLayout, + endContextualContainer: ViewGroup, + startContextualContainer: ViewGroup +) : + AbstractNavButtonLayoutter( + resources, + navBarContainer, + endContextualContainer, + startContextualContainer + ) { + + override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) { + // TODO(b/230395757): Polish pending, this is just to make it usable + val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams + val taskbarDimensions = + DimensionUtils.getTaskbarPhoneDimensions(dp, resources, TaskbarManager.isPhoneMode(dp)) + val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) + navContainerParams.width = taskbarDimensions.x + navContainerParams.height = ViewGroup.LayoutParams.MATCH_PARENT + navContainerParams.gravity = Gravity.CENTER_VERTICAL + + // Ensure order of buttons is correct + navButtonContainer.removeAllViews() + navButtonContainer.orientation = LinearLayout.HORIZONTAL + navContainerParams.topMargin = 0 + navContainerParams.bottomMargin = 0 + navContainerParams.marginEnd = endStartMargins + navContainerParams.marginStart = endStartMargins + // Swap recents and back button in case we were landscape prior to this + navButtonContainer.addView(backButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(recentsButton) + + navButtonContainer.layoutParams = navContainerParams + + // Add the spaces in between the nav buttons + val spaceInBetween = + resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween_phone) + for (i in 0 until navButtonContainer.childCount) { + val navButton = navButtonContainer.getChildAt(i) + val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams + buttonLayoutParams.weight = 1f + when (i) { + 0 -> { + // First button + buttonLayoutParams.marginEnd = spaceInBetween / 2 + } + navButtonContainer.childCount - 1 -> { + // Last button + buttonLayoutParams.marginStart = spaceInBetween / 2 + } + else -> { + // other buttons + buttonLayoutParams.marginStart = spaceInBetween / 2 + buttonLayoutParams.marginEnd = spaceInBetween / 2 + } + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt new file mode 100644 index 0000000000..a24002c44d --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt @@ -0,0 +1,50 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.android.launcher3.DeviceProfile + +class SetupNavLayoutter( + resources: Resources, + navButtonContainer: LinearLayout, + endContextualContainer: ViewGroup, + startContextualContainer: ViewGroup +) : + AbstractNavButtonLayoutter( + resources, + navButtonContainer, + endContextualContainer, + startContextualContainer + ) { + + override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) { + // Since setup wizard only has back button enabled, it looks strange to be + // end-aligned, so start-align instead. + val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams + navButtonsLayoutParams.apply { + marginStart = navButtonsLayoutParams.marginEnd + marginEnd = 0 + gravity = Gravity.START + } + navButtonContainer.requestLayout() + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt new file mode 100644 index 0000000000..5ec7ca0e2e --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt @@ -0,0 +1,82 @@ +/* + * 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.navbutton + +import android.content.res.Resources +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R + +/** Layoutter for showing 3 button navigation on large screen */ +class TaskbarNavLayoutter( + resources: Resources, + navBarContainer: LinearLayout, + endContextualContainer: ViewGroup, + startContextualContainer: ViewGroup +) : + AbstractNavButtonLayoutter( + resources, + navBarContainer, + endContextualContainer, + startContextualContainer + ) { + + override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) { + // Add spacing after the end of the last nav button + val navButtonParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams + var navMarginEnd = resources.getDimension(dp.inv.inlineNavButtonsEndSpacing).toInt() + val contextualWidth = endContextualContainer.width + // If contextual buttons are showing, we check if the end margin is enough for the + // contextual button to be showing - if not, move the nav buttons over a smidge + if (isContextualButtonShowing && navMarginEnd < contextualWidth) { + // Additional spacing, eat up half of space between last icon and nav button + navMarginEnd += resources.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing) / 2 + } + + navButtonParams.apply { + gravity = Gravity.END + width = FrameLayout.LayoutParams.WRAP_CONTENT + height = ViewGroup.LayoutParams.MATCH_PARENT + marginEnd = navMarginEnd + } + navButtonContainer.orientation = LinearLayout.HORIZONTAL + navButtonContainer.layoutParams = navButtonParams + + // Add the spaces in between the nav buttons + val spaceInBetween = resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween) + for (i in 0 until navButtonContainer.childCount) { + val navButton = navButtonContainer.getChildAt(i) + val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams + buttonLayoutParams.weight = 0f + when (i) { + 0 -> { + buttonLayoutParams.marginEnd = spaceInBetween / 2 + } + navButtonContainer.childCount - 1 -> { + buttonLayoutParams.marginStart = spaceInBetween / 2 + } + else -> { + buttonLayoutParams.marginStart = spaceInBetween / 2 + buttonLayoutParams.marginEnd = spaceInBetween / 2 + } + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java new file mode 100644 index 0000000000..66d591889b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java @@ -0,0 +1,142 @@ +/* + * 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.overlay; + +import android.content.Context; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.dot.DotInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.taskbar.BaseTaskbarContext; +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.TaskbarDragController; +import com.android.launcher3.taskbar.TaskbarStashController; +import com.android.launcher3.taskbar.TaskbarUIController; +import com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView; +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; + +/** + * Window context for the taskbar overlays such as All Apps and EDU. + *

+ * Overlays have their own window and need a window context. Some properties are delegated to the + * {@link TaskbarActivityContext} such as {@link PopupDataProvider}. + */ +public class TaskbarOverlayContext extends BaseTaskbarContext { + private final TaskbarActivityContext mTaskbarContext; + + private final TaskbarOverlayController mOverlayController; + private final TaskbarDragController mDragController; + private final TaskbarOverlayDragLayer mDragLayer; + + // We automatically stash taskbar when All Apps is opened in gesture navigation mode. + private final boolean mWillTaskbarBeVisuallyStashed; + private final int mStashedTaskbarHeight; + private final TaskbarUIController mUiController; + + public TaskbarOverlayContext( + Context windowContext, + TaskbarActivityContext taskbarContext, + TaskbarControllers controllers) { + super(windowContext); + mTaskbarContext = taskbarContext; + mOverlayController = controllers.taskbarOverlayController; + mDragController = new TaskbarDragController(this); + mDragController.init(controllers); + mDragLayer = new TaskbarOverlayDragLayer(this); + + TaskbarStashController taskbarStashController = controllers.taskbarStashController; + mWillTaskbarBeVisuallyStashed = taskbarStashController.supportsVisualStashing(); + mStashedTaskbarHeight = taskbarStashController.getStashedHeight(); + + mUiController = controllers.uiController; + } + + boolean willTaskbarBeVisuallyStashed() { + return mWillTaskbarBeVisuallyStashed; + } + + int getStashedTaskbarHeight() { + return mStashedTaskbarHeight; + } + + public TaskbarOverlayController getOverlayController() { + return mOverlayController; + } + + @Override + public DeviceProfile getDeviceProfile() { + return mOverlayController.getLauncherDeviceProfile(); + } + + @Override + public TaskbarDragController getDragController() { + return mDragController; + } + + @Override + public TaskbarOverlayDragLayer getDragLayer() { + return mDragLayer; + } + + @Override + public TaskbarAllAppsContainerView getAppsView() { + return mDragLayer.findViewById(R.id.apps_view); + } + + @Override + public boolean isBindingItems() { + return mTaskbarContext.isBindingItems(); + } + + @Override + public View.OnClickListener getItemOnClickListener() { + return mTaskbarContext.getItemOnClickListener(); + } + + @Override + public PopupDataProvider getPopupDataProvider() { + return mTaskbarContext.getPopupDataProvider(); + } + + @Override + public void startSplitSelection(SplitSelectSource splitSelectSource) { + mUiController.startSplitSelection(splitSelectSource); + } + + @Override + public DotInfo getDotInfoForItem(ItemInfo info) { + return mTaskbarContext.getDotInfoForItem(info); + } + + @Override + public void onDragStart() {} + + @Override + public void onDragEnd() { + mOverlayController.maybeCloseWindow(); + } + + @Override + public void onPopupVisibilityChanged(boolean isVisible) {} + + @Override + public void onSplitScreenMenuButtonClicked() { + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java new file mode 100644 index 0000000000..476e0a8bab --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java @@ -0,0 +1,213 @@ +/* + * 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.overlay; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; +import static com.android.launcher3.LauncherState.ALL_APPS; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PixelFormat; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; + +import androidx.annotation.Nullable; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; + +import java.util.Optional; + +/** + * Handles the Taskbar overlay window lifecycle. + *

+ * Overlays need to be inflated in a separate window so that have the correct hierarchy. For + * instance, they need to be below the notification tray. If there are multiple overlays open, the + * same window is used. + */ +public final class TaskbarOverlayController { + + private static final String WINDOW_TITLE = "Taskbar Overlay"; + + private final TaskbarActivityContext mTaskbarContext; + private final Context mWindowContext; + private final TaskbarOverlayProxyView mProxyView; + private final LayoutParams mLayoutParams; + + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onTaskStackChanged() { + mProxyView.close(false); + } + + @Override + public void onTaskMovedToFront(int taskId) { + mProxyView.close(false); + } + }; + + private DeviceProfile mLauncherDeviceProfile; + private @Nullable TaskbarOverlayContext mOverlayContext; + private TaskbarControllers mControllers; // Initialized in init. + + public TaskbarOverlayController( + TaskbarActivityContext taskbarContext, DeviceProfile launcherDeviceProfile) { + mTaskbarContext = taskbarContext; + mWindowContext = mTaskbarContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null); + mProxyView = new TaskbarOverlayProxyView(); + mLayoutParams = createLayoutParams(); + mLauncherDeviceProfile = launcherDeviceProfile; + } + + /** Initialize the controller. */ + public void init(TaskbarControllers controllers) { + mControllers = controllers; + } + + /** + * Creates a window for Taskbar overlays, if it does not already exist. Returns the window + * context for the current overlay window. + */ + public TaskbarOverlayContext requestWindow() { + if (mOverlayContext == null) { + mOverlayContext = new TaskbarOverlayContext( + mWindowContext, mTaskbarContext, mControllers); + } + + if (!mProxyView.isOpen()) { + mProxyView.show(); + Optional.ofNullable(mOverlayContext.getSystemService(WindowManager.class)) + .ifPresent(m -> m.addView(mOverlayContext.getDragLayer(), mLayoutParams)); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); + } + + return mOverlayContext; + } + + /** Hides the current overlay window with animation. */ + public void hideWindow() { + mProxyView.close(true); + } + + /** + * Removes the overlay window from the hierarchy, if all floating views are closed and there is + * no system drag operation in progress. + *

+ * This method should be called after an exit animation finishes, if applicable. + */ + @SuppressLint("WrongConstant") + void maybeCloseWindow() { + if (mOverlayContext != null && (AbstractFloatingView.hasOpenView(mOverlayContext, TYPE_ALL) + || mOverlayContext.getDragController().isSystemDragInProgress())) { + return; + } + mProxyView.close(false); + onDestroy(); + } + + /** Destroys the controller and any overlay window if present. */ + public void onDestroy() { + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); + Optional.ofNullable(mOverlayContext) + .map(c -> c.getSystemService(WindowManager.class)) + .ifPresent(m -> m.removeViewImmediate(mOverlayContext.getDragLayer())); + mOverlayContext = null; + } + + /** The current device profile for the overlay window. */ + public DeviceProfile getLauncherDeviceProfile() { + return mLauncherDeviceProfile; + } + + /** Updates {@link DeviceProfile} instance for Taskbar's overlay window. */ + public void updateLauncherDeviceProfile(DeviceProfile dp) { + mLauncherDeviceProfile = dp; + Optional.ofNullable(mOverlayContext).ifPresent(c -> { + AbstractFloatingView.closeAllOpenViewsExcept(c, false, TYPE_REBIND_SAFE); + c.dispatchDeviceProfileChanged(); + }); + } + + /** The default open duration for overlays. */ + public int getOpenDuration() { + return ALL_APPS.getTransitionDuration(mTaskbarContext, true); + } + + /** The default close duration for overlays. */ + public int getCloseDuration() { + return ALL_APPS.getTransitionDuration(mTaskbarContext, false); + } + + @SuppressLint("WrongConstant") + private LayoutParams createLayoutParams() { + LayoutParams layoutParams = new LayoutParams( + TYPE_APPLICATION_OVERLAY, + LayoutParams.FLAG_SPLIT_TOUCH, + PixelFormat.TRANSLUCENT); + layoutParams.setTitle(WINDOW_TITLE); + layoutParams.gravity = Gravity.BOTTOM; + layoutParams.packageName = mTaskbarContext.getPackageName(); + layoutParams.setFitInsetsTypes(0); // Handled by container view. + layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + layoutParams.setSystemApplicationOverlay(true); + return layoutParams; + } + + /** + * Proxy view connecting taskbar drag layer to the overlay window. + * + * Overlays are in a separate window and has its own drag layer, but this proxy lets its views + * behave as though they are in the taskbar drag layer. For instance, when the taskbar closes + * all {@link AbstractFloatingView} instances, the overlay window will also close. + */ + private class TaskbarOverlayProxyView extends AbstractFloatingView { + + private TaskbarOverlayProxyView() { + super(mTaskbarContext, null); + } + + private void show() { + mIsOpen = true; + mTaskbarContext.getDragLayer().addView(this); + } + + @Override + protected void handleClose(boolean animate) { + mTaskbarContext.getDragLayer().removeView(this); + Optional.ofNullable(mOverlayContext).ifPresent(c -> closeAllOpenViews(c, animate)); + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_TASKBAR_OVERLAY_PROXY) != 0; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return false; + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java new file mode 100644 index 0000000000..ec64128c91 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java @@ -0,0 +1,203 @@ +/* + * 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.overlay; + +import static android.view.KeyEvent.ACTION_UP; +import static android.view.KeyEvent.KEYCODE_BACK; +import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; + +import android.content.Context; +import android.graphics.Insets; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; + +import androidx.annotation.NonNull; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** Root drag layer for the Taskbar overlay window. */ +public class TaskbarOverlayDragLayer extends + BaseDragLayer implements + ViewTreeObserver.OnComputeInternalInsetsListener { + + private final List mOnClickListeners = new CopyOnWriteArrayList<>(); + private final TouchController mClickListenerTouchController = new TouchController() { + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + if (ev.getActionMasked() == MotionEvent.ACTION_UP) { + for (OnClickListener listener : mOnClickListeners) { + listener.onClick(TaskbarOverlayDragLayer.this); + } + } + return false; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + for (int i = 0; i < getChildCount(); i++) { + if (isEventOverView(getChildAt(i), ev)) { + return false; + } + } + return true; + } + }; + + TaskbarOverlayDragLayer(Context context) { + super(context, null, 1); + setClipChildren(false); + recreateControllers(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + + @Override + public void recreateControllers() { + mControllers = mOnClickListeners.isEmpty() + ? new TouchController[]{mActivity.getDragController()} + : new TouchController[] { + mActivity.getDragController(), mClickListenerTouchController}; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev); + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { + AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); + if (topView != null && topView.canHandleBack()) { + topView.onBackInvoked(); + return true; + } + } + return super.dispatchKeyEvent(event); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + if (mActivity.getDragController().isSystemDragInProgress()) { + inoutInfo.touchableRegion.setEmpty(); + inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); + } + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + return updateInsetsDueToStashing(insets); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + mActivity.getOverlayController().maybeCloseWindow(); + } + + /** + * Adds the given callback to clicks to this drag layer. + *

+ * Clicks are only accepted on this drag layer if they fall within this drag layer's bounds and + * outside the bounds of all child views. + *

+ * If the click falls within the bounds of a child view, then this callback does not run and + * that child can optionally handle it. + */ + private void addOnClickListener(@NonNull OnClickListener listener) { + boolean wasEmpty = mOnClickListeners.isEmpty(); + mOnClickListeners.add(listener); + if (wasEmpty) { + recreateControllers(); + } + } + + /** + * Removes the given on click callback. + *

+ * No-op if the callback was never added. + */ + private void removeOnClickListener(@NonNull OnClickListener listener) { + boolean wasEmpty = mOnClickListeners.isEmpty(); + mOnClickListeners.remove(listener); + if (!wasEmpty && mOnClickListeners.isEmpty()) { + recreateControllers(); + } + } + + /** + * Queues the given callback on the next click on this drag layer. + *

+ * Once run, this callback is immediately removed. + */ + public void runOnClickOnce(@NonNull OnClickListener listener) { + addOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + listener.onClick(v); + removeOnClickListener(this); + } + }); + } + + /** + * Taskbar automatically stashes when opening all apps, but we don't report the insets as + * changing to avoid moving the underlying app. But internally, the apps view should still + * layout according to the stashed insets rather than the unstashed insets. So this method + * does two things: + * 1) Sets navigationBars bottom inset to stashedHeight. + * 2) Sets tappableInsets bottom inset to 0. + */ + private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) { + if (!mActivity.willTaskbarBeVisuallyStashed()) { + return oldInsets; + } + WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets); + + Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); + Insets newNavInsets = Insets.of(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right, + mActivity.getStashedTaskbarHeight()); + updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); + + Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); + Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, + oldTappableInsets.right, 0); + updatedInsetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); + + return updatedInsetsBuilder.build(); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/unfold/NonDestroyableScopedUnfoldTransitionProgressProvider.java b/quickstep/src/com/android/launcher3/taskbar/unfold/NonDestroyableScopedUnfoldTransitionProgressProvider.java new file mode 100644 index 0000000000..f9da4e4e74 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/unfold/NonDestroyableScopedUnfoldTransitionProgressProvider.java @@ -0,0 +1,30 @@ +/* + * 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.unfold; + +import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; + +/** + * ScopedUnfoldTransitionProgressProvider that doesn't propagate destroy method + */ +public class NonDestroyableScopedUnfoldTransitionProgressProvider extends + ScopedUnfoldTransitionProgressProvider { + + @Override + public void destroy() { + // no-op + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java index afb06f1fe0..d22b32a66a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java +++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java @@ -17,61 +17,21 @@ package com.android.launcher3.uioverrides; import android.app.Person; -import android.content.Context; import android.content.pm.ShortcutInfo; -import android.content.res.Resources; -import android.view.Display; -import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; +/** + * A wrapper for the hidden API calls + */ public class ApiWrapper { public static final boolean TASKBAR_DRAWN_IN_PROCESS = true; public static Person[] getPersons(ShortcutInfo si) { - if (!Utilities.ATLEAST_Q) return Utilities.EMPTY_PERSON_ARRAY; + if (!Utilities.ATLEAST_Q) + return Utilities.EMPTY_PERSON_ARRAY; Person[] persons = si.getPersons(); return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons; } - - /** - * Returns true if the display is an internal displays - */ - public static boolean isInternalDisplay(Display display) { - return display.getType() == Display.TYPE_INTERNAL; - } - - /** - * Returns a unique ID representing the display - */ - public static String getUniqueId(Display display) { - try { - return display.getUniqueId(); - } catch (Throwable t) { - return "" + display.getDisplayId(); - } - } - - /** - * Returns the minimum space that should be left empty at the end of hotseat - */ - public static int getHotseatEndOffset(Context context) { - if (SysUINavigationMode.INSTANCE.get(context).getMode() == Mode.THREE_BUTTONS) { - Resources res = context.getResources(); - /* - * 3 nav buttons + - * Little space at the end for contextual buttons + - * Little space between icons and nav buttons - */ - return 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) - + res.getDimensionPixelSize(R.dimen.taskbar_contextual_button_margin) - + res.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing); - } else { - return 0; - } - - } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java index 84b3839825..955440b49a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java @@ -16,6 +16,7 @@ package com.android.launcher3.uioverrides; +import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME; import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT; import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; import static com.android.launcher3.anim.Interpolators.INSTANT; @@ -23,25 +24,32 @@ import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y; import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; +import static com.android.quickstep.views.FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; +import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA; +import android.graphics.Rect; +import android.graphics.RectF; import android.util.FloatProperty; -import android.util.Log; +import android.view.animation.Interpolator; import androidx.annotation.NonNull; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; +import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.RecentsView; /** @@ -53,9 +61,9 @@ import com.android.quickstep.views.RecentsView; public abstract class BaseRecentsViewStateController implements StateHandler { protected final T mRecentsView; - protected final BaseQuickstepLauncher mLauncher; + protected final QuickstepLauncher mLauncher; - public BaseRecentsViewStateController(@NonNull BaseQuickstepLauncher launcher) { + public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) { mLauncher = launcher; mRecentsView = launcher.getOverviewPanel(); } @@ -67,24 +75,25 @@ public abstract class BaseRecentsViewStateController ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]); TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f); - float recentsAlpha = state.overviewUi ? 1f : 0; - Log.d(BAD_STATE, "BaseRecentsViewStateController setState state=" + state - + ", alpha=" + recentsAlpha); - getContentAlphaProperty().set(mRecentsView, recentsAlpha); + getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0); getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness()); RECENTS_GRID_PROGRESS.set(mRecentsView, state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f); + TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f); } @Override public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, PendingAnimation builder) { - Log.d(BAD_STATE, "BaseRecentsViewStateController setStateWithAnimation state=" + toState - + ", config.skipOverview=" + config.hasAnimationFlag(SKIP_OVERVIEW)); if (config.hasAnimationFlag(SKIP_OVERVIEW)) { return; } setStateWithAnimationInternal(toState, config, builder); + builder.addEndListener(success -> { + if (!success) { + mRecentsView.reset(); + } + }); } /** @@ -104,19 +113,70 @@ public abstract class BaseRecentsViewStateController setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f, config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR)); - float recentsAlpha = toState.overviewUi ? 1 : 0; - Log.d(BAD_STATE, "BaseRecentsViewStateController setStateWithAnimationInternal toState=" - + toState + ", alpha=" + recentsAlpha); - setter.setFloat(mRecentsView, getContentAlphaProperty(), recentsAlpha, + if (mRecentsView.isSplitSelectionActive()) { + // TODO (b/238651489): Refactor state management to avoid need for double check + FloatingTaskView floatingTask = mRecentsView.getFirstFloatingTaskView(); + if (floatingTask != null) { + // We are in split selection state currently, transitioning to another state + DragLayer dragLayer = mLauncher.getDragLayer(); + RectF onScreenRectF = new RectF(); + Utilities.getBoundsForViewInDragLayer(mLauncher.getDragLayer(), floatingTask, + new Rect(0, 0, floatingTask.getWidth(), floatingTask.getHeight()), + false, null, onScreenRectF); + // Get the part of the floatingTask that intersects with the DragLayer (i.e. the + // on-screen portion) + onScreenRectF.intersect( + dragLayer.getLeft(), + dragLayer.getTop(), + dragLayer.getRight(), + dragLayer.getBottom() + ); + + setter.setFloat( + mRecentsView.getFirstFloatingTaskView(), + PRIMARY_TRANSLATE_OFFSCREEN, + mRecentsView.getPagedOrientationHandler() + .getFloatingTaskOffscreenTranslationTarget( + floatingTask, + onScreenRectF, + floatingTask.getStagePosition(), + mLauncher.getDeviceProfile() + ), + config.getInterpolator( + ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN, + LINEAR + )); + setter.setViewAlpha( + mRecentsView.getSplitInstructionsView(), + 0, + config.getInterpolator( + ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, + LINEAR + ) + ); + } + } + + setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0, config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); setter.setFloat( mRecentsView, getTaskModalnessProperty(), toState.getOverviewModalness(), config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR)); + + LauncherState fromState = mLauncher.getStateManager().getState(); + setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA, + toState.showTaskThumbnailSplash() ? 1f : 0f, + !toState.showTaskThumbnailSplash() && fromState == QUICK_SWITCH_FROM_HOME + ? LINEAR : INSTANT); + boolean showAsGrid = toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()); + Interpolator gridProgressInterpolator = showAsGrid + ? fromState == QUICK_SWITCH_FROM_HOME ? LINEAR : INSTANT + : FINAL_FRAME; setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, showAsGrid ? 1f : 0f, - showAsGrid ? INSTANT : FINAL_FRAME); + gridProgressInterpolator); } abstract FloatProperty getTaskModalnessProperty(); diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index ee6e8cee9b..f3663cd19c 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -16,6 +16,7 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; +import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; import android.animation.Animator; import android.animation.AnimatorSet; @@ -48,10 +49,12 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.icons.IconNormalizer; import com.android.launcher3.icons.LauncherIcons; +import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.touch.ItemClickHandler; import com.android.launcher3.touch.ItemLongClickListener; @@ -115,14 +118,16 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { this(context, attrs, 0); } + Context mContext; public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mContext = context; mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile(); mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize()); int shadowSize = context.getResources().getDimensionPixelSize( R.dimen.blur_size_thin_outline); mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER); - mShapePath = GraphicsUtils.getShapePath(mNormalizedIconSize); + mShapePath = GraphicsUtils.getShapePath(context, mNormalizedIconSize); } @Override @@ -179,7 +184,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { : null; super.applyFromWorkspaceItem(info, animate, staggerIndex); int oldPlateColor = mPlateColor; - int newPlateColor = ColorUtils.setAlphaComponent(mDotParams.color, 200); + int newPlateColor = ColorUtils.setAlphaComponent(mDotParams.appColor, 200); if (!animate) { mPlateColor = newPlateColor; } @@ -236,7 +241,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2); mSlotMachineIcons.add(getIcon()); iconsToAnimate.stream() - .map(iconInfo -> iconInfo.newThemedIcon(mContext)) + .map(iconInfo -> iconInfo.newIcon(mContext)) .forEach(mSlotMachineIcons::add); if (endWithOriginalIcon) { mSlotMachineIcons.add(getIcon()); @@ -270,7 +275,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { mIsPinned = true; applyFromWorkspaceItem(info); setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); - ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true; + ((CellLayoutLayoutParams) getLayoutParams()).canReorder = true; invalidate(); } @@ -279,7 +284,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { */ public void finishBinding(OnLongClickListener longClickListener) { setOnLongClickListener(longClickListener); - ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false; + ((CellLayoutLayoutParams) getLayoutParams()).canReorder = false; setTextVisibility(false); verifyHighRes(); } @@ -358,6 +363,19 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { canvas.drawPath(mRingPath, mIconRingPaint); } + @Override + public void setIconDisabled(boolean isDisabled) { + super.setIconDisabled(isDisabled); + mIconRingPaint.setColorFilter(isDisabled ? getDisabledColorFilter() : null); + invalidate(); + } + + @Override + protected void setItemInfo(ItemInfoWithIcon itemInfo) { + super.setItemInfo(itemInfo); + setIconDisabled(itemInfo.isDisabled()); + } + @Override public void getSourceVisualDragBounds(Rect bounds) { super.getSourceVisualDragBounds(bounds); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java new file mode 100644 index 0000000000..6659fa0ee7 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java @@ -0,0 +1,70 @@ +/** + * 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.uioverrides; + +import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.LauncherWidgetHolder; + +import java.util.function.IntConsumer; + +/** + * {@link AppWidgetHost} that is used to receive the changes to the widgets without + * storing any {@code Activity} info like that of the launcher. + */ +final class QuickstepAppWidgetHost extends AppWidgetHost { + private final @NonNull Context mContext; + private final @NonNull IntConsumer mAppWidgetRemovedCallback; + private final @NonNull LauncherWidgetHolder.ProviderChangedListener mProvidersChangedListener; + + QuickstepAppWidgetHost(@NonNull Context context, @NonNull IntConsumer appWidgetRemovedCallback, + @NonNull LauncherWidgetHolder.ProviderChangedListener listener, + @NonNull Looper looper) { + super(context, APPWIDGET_HOST_ID, null, looper); + mContext = context; + mAppWidgetRemovedCallback = appWidgetRemovedCallback; + mProvidersChangedListener = listener; + } + + @Override + protected void onProvidersChanged() { + mProvidersChangedListener.notifyWidgetProvidersChanged(); + } + + @Override + public void onAppWidgetRemoved(int appWidgetId) { + mAppWidgetRemovedCallback.accept(appWidgetId); + } + + @Override + protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) { + LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( + mContext, appWidget); + super.onProviderChanged(appWidgetId, info); + // The super method updates the dimensions of the providerInfo. Update the + // launcher spans accordingly. + info.initSpans(mContext, LauncherAppState.getIDP(mContext)); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java index 757bb9c9d9..df0f204135 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java @@ -36,6 +36,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.widget.LauncherAppWidgetHostView; +import app.lawnchair.LawnchairApp; import dev.rikka.tools.refine.Refine; /** Provides a Quickstep specific animation when launching an activity from an app widget. */ @@ -68,7 +69,7 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { launchCookie = mLauncher.getLaunchCookie((ItemInfo) itemInfo); activityOptions.options.setLaunchCookie(launchCookie); } - if (Utilities.ATLEAST_S && !pendingIntent.isActivity()) { + if (Utilities.ATLEAST_S && !pendingIntent.isActivity() && LawnchairApp.isRecentsEnabled ()) { // In the event this pending intent eventually launches an activity, i.e. a trampoline, // use the Quickstep transition animation. try { @@ -86,9 +87,10 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { } catch (RemoteException e) { // Do nothing. } + activityOptions.options.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR); } - activityOptions.options.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activityOptions.options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_EMPTY); + options = Pair.create(options.first, activityOptions.options); if (pendingIntent.isActivity()) { logAppLaunch(itemInfo); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index b0924faf85..143f1a9ecb 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,50 +14,116 @@ * limitations under the License. */ package com.android.launcher3.uioverrides; - +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 com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON; +import static com.android.launcher3.LauncherSettings.Animation.VIEW_BACKGROUND; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.NO_OFFSET; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; +import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_WIDGET_APP_START; +import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE; +import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE; +import static com.android.launcher3.config.FeatureFlags.RECEIVE_UNFOLD_EVENTS_FROM_SYSUI; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; -import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL; -import static com.android.launcher3.testing.TestProtocol.HINT_STATE_TWO_BUTTON_ORDINAL; -import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL; -import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL; +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.INSTALL; +import static com.android.launcher3.popup.SystemShortcut.WIDGETS; +import static com.android.launcher3.taskbar.LauncherTaskbarUIController.ALL_APPS_PAGE_PROGRESS_INDEX; +import static com.android.launcher3.taskbar.LauncherTaskbarUIController.MINUS_ONE_PAGE_PROGRESS_INDEX; +import static com.android.launcher3.taskbar.LauncherTaskbarUIController.WIDGETS_PAGE_PROGRESS_INDEX; +import static com.android.launcher3.testing.shared.TestProtocol.HINT_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.HINT_STATE_TWO_BUTTON_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.QUICK_SWITCH_STATE_ORDINAL; +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.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; +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 android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.SensorManager; +import android.hardware.devicestate.DeviceStateManager; +import android.hardware.display.DisplayManager; +import android.media.permission.SafeCloseable; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.IBinder; +import android.os.SystemProperties; +import android.os.Trace; +import android.view.Display; import android.view.HapticFeedbackConstants; +import android.view.RemoteAnimationTarget; import android.view.View; - -import com.android.launcher3.BaseQuickstepLauncher; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedDispatcher; +import android.window.SplashScreen; +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +//import com.android.app.viewcapture.SettingsAwareViewCapture; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherState; +import com.android.launcher3.OnBackPressedHandler; import com.android.launcher3.QuickstepAccessibilityDelegate; +import com.android.launcher3.QuickstepTransitionManager; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.appprediction.PredictionRowView; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.hybridhotseat.HotseatPredictionController; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.BgDataModel.FixedContainerItems; +import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.proxy.ProxyActivityStarter; +import com.android.launcher3.proxy.StartActivityParams; +import com.android.launcher3.statehandlers.DepthController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory; +import com.android.launcher3.statemanager.StateManager.StateHandler; +import com.android.launcher3.taskbar.LauncherTaskbarUIController; +import com.android.launcher3.taskbar.TaskbarManager; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory; import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory; import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController; import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController; @@ -68,59 +134,125 @@ import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchControll import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController; -import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.OnboardingPrefs; +import com.android.launcher3.util.ActivityOptionsWrapper; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.IntSet; +import com.android.launcher3.util.NavigationMode; +import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.PendingRequestArgs; +import com.android.launcher3.util.PendingSplitSelectInfo; +import com.android.launcher3.util.RunnableList; +import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.launcher3.util.TouchController; -import com.android.launcher3.util.UiThreadHelper; -import com.android.launcher3.util.UiThreadHelper.AsyncCommand; -import com.android.launcher3.widget.LauncherAppWidgetHost; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; +import com.android.launcher3.widget.LauncherWidgetHolder; +import com.android.quickstep.OverviewCommandHelper; +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.GroupTask; +import com.android.quickstep.util.LauncherUnfoldAnimationController; +import com.android.quickstep.util.ProxyScreenStatusProvider; import com.android.quickstep.util.QuickstepOnboardingPrefs; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteFadeOutAnimationListener; +import com.android.quickstep.util.SplitSelectStateController; +import com.android.quickstep.util.SplitToWorkspaceController; +import com.android.quickstep.util.SplitWithKeyboardShortcutController; +import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.views.DesktopTaskView; +import com.android.quickstep.views.FloatingTaskView; +import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; - +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.unfold.RemoteUnfoldSharedComponent; +import com.android.systemui.unfold.UnfoldSharedComponent; +import com.android.systemui.unfold.UnfoldTransitionFactory; +import com.android.systemui.unfold.UnfoldTransitionProgressProvider; +import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig; +import com.android.systemui.unfold.config.UnfoldTransitionConfig; +import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver; +import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider; +import com.android.systemui.unfold.system.DeviceStateManagerFoldProvider; +import com.android.systemui.unfold.updates.RotationChangeProvider; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.Stream; - -import app.lawnchair.LawnchairApp; - -public class QuickstepLauncher extends BaseQuickstepLauncher { - +public class QuickstepLauncher extends Launcher { + public static final boolean ENABLE_PIP_KEEP_CLEAR_ALGORITHM = + SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true); public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false; - /** - * Reusable command for applying the shelf height on the background thread. - */ - public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) -> - SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2); - private FixedContainerItems mAllAppsPredictions; private HotseatPredictionController mHotseatPredictionController; - + private DepthController mDepthController; + private DesktopVisibilityController mDesktopVisibilityController; + private QuickstepTransitionManager mAppTransitionManager; + private OverviewActionsView mActionsView; + private TISBindHelper mTISBindHelper; + private @Nullable TaskbarManager mTaskbarManager; + private @Nullable OverviewCommandHelper mOverviewCommandHelper; + private @Nullable LauncherTaskbarUIController mTaskbarUIController; + // Will be updated when dragging from taskbar. + private @Nullable DragOptions mNextWorkspaceDragOptions = null; + private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider; + private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController; + private SplitSelectStateController mSplitSelectStateController; + private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController; + private SplitToWorkspaceController mSplitToWorkspaceController; + /** + * If Launcher restarted while in the middle of an Overview split select, it needs this data to + * recover. In all other cases this will remain null. + */ + private PendingSplitSelectInfo mPendingSplitSelectInfo = null; + private SafeCloseable mViewCapture; + private boolean mEnableWidgetDepth; @Override protected void setupViews() { super.setupViews(); + mActionsView = findViewById(R.id.overview_actions_view); + RecentsView overviewPanel = (RecentsView) getOverviewPanel(); + mSplitSelectStateController = + new SplitSelectStateController(this, mHandler, getStateManager(), + getDepthController(), getStatsLogManager(), + SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this)); + overviewPanel.init(mActionsView, mSplitSelectStateController); + mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this, + mSplitSelectStateController); + mSplitToWorkspaceController = new SplitToWorkspaceController(this, + mSplitSelectStateController); + mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize()); + mActionsView.updateVerticalMargin(DisplayController.getNavigationMode(this)); + mAppTransitionManager = buildAppTransitionManager(); + mAppTransitionManager.registerRemoteAnimations(); + mAppTransitionManager.registerRemoteTransitions(); + mTISBindHelper = new TISBindHelper(this, this::onTISConnected); + mDepthController = new DepthController(this); + mDesktopVisibilityController = new DesktopVisibilityController(this); mHotseatPredictionController = new HotseatPredictionController(this); + mEnableWidgetDepth = SystemProperties.getBoolean("ro.launcher.depth.widget", true); + getWorkspace().addOverlayCallback(progress -> + onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX)); } - @Override public void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info, - InstanceId instanceId) { + InstanceId instanceId) { // If the app launch is from any of the surfaces in AllApps then add the InstanceId from // LiveSearchManager to recreate the AllApps session on the server side. if (mAllAppsSessionLogId != null && ALL_APPS.equals( getStateManager().getCurrentStableState())) { instanceId = mAllAppsSessionLogId; } - StatsLogger logger = statsLogManager.logger().withItemInfo(info).withInstanceId(instanceId); - if (mAllAppsPredictions != null && (info.itemType == ITEM_TYPE_APPLICATION || info.itemType == ITEM_TYPE_SHORTCUT @@ -134,46 +266,49 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { logger.withRank(i); break; } - } } logger.log(LAUNCHER_APP_LAUNCH_TAP); - mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId); } - @Override protected void completeAddShortcut(Intent data, int container, int screenId, int cellX, - int cellY, PendingRequestArgs args) { + int cellY, PendingRequestArgs args) { if (container == CONTAINER_HOTSEAT) { mHotseatPredictionController.onDeferredDrop(cellX, cellY); } super.completeAddShortcut(data, container, screenId, cellX, cellY, args); } - @Override protected LauncherAccessibilityDelegate createAccessibilityDelegate() { return new QuickstepAccessibilityDelegate(this); } - /** * Returns Prediction controller for hybrid hotseat */ public HotseatPredictionController getHotseatPredictionController() { return mHotseatPredictionController; } - @Override - protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) { + public void enableHotseatEdu(boolean enable) { + super.enableHotseatEdu(enable); + mHotseatPredictionController.enableHotseatEdu(enable); + } + /** + * Builds the {@link QuickstepTransitionManager} instance to use for managing transitions. + */ + protected QuickstepTransitionManager buildAppTransitionManager() { + return new QuickstepTransitionManager(this); + } + @Override + protected QuickstepOnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) { return new QuickstepOnboardingPrefs(this, sharedPrefs); } - @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); onStateOrResumeChanging(false /* inTransition */); } - @Override public boolean startActivitySafely(View v, Intent intent, ItemInfo item) { // Only pause is taskbar controller is not present @@ -184,33 +319,65 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { } return started; } - @Override protected void onActivityFlagsChanged(int changeBits) { + if ((changeBits & ACTIVITY_STATE_STARTED) != 0) { + mDepthController.setActivityStarted(isStarted()); + } + if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) { + if (mTaskbarUIController != null) { + mTaskbarUIController.onLauncherResumedOrPaused(hasBeenResumed()); + } + } super.onActivityFlagsChanged(changeBits); if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) { onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0); } - if (((changeBits & ACTIVITY_STATE_STARTED) != 0 || (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) { mHotseatPredictionController.setPauseUIUpdate(false); } } - @Override protected void showAllAppsFromIntent(boolean alreadyOnHome) { TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); super.showAllAppsFromIntent(alreadyOnHome); } - + protected void onItemClicked(View view) { + if (!mSplitToWorkspaceController.handleSecondAppSelectionForSplit(view)) { + QuickstepLauncher.super.getItemOnClickListener().onClick(view); + } + } + @Override + public View.OnClickListener getItemOnClickListener() { + return this::onItemClicked; + } @Override public Stream getSupportedShortcuts() { - return Stream.concat( - Stream.of(mHotseatPredictionController), super.getSupportedShortcuts()); + // Order matters as it affects order of appearance in popup container + List shortcuts = new ArrayList(Arrays.asList( + APP_INFO, WellbeingModel.SHORTCUT_FACTORY, mHotseatPredictionController)); + shortcuts.addAll(getSplitShortcuts()); + shortcuts.add(WIDGETS); + shortcuts.add(INSTALL); + return shortcuts.stream(); + } + private List> getSplitShortcuts() { + if (!ENABLE_SPLIT_FROM_WORKSPACE.get() || !mDeviceProfile.isTablet) { + return Collections.emptyList(); + } + RecentsView recentsView = (RecentsView) getOverviewPanel(); + // TODO(b/266482558): Pull it out of PagedOrentationHandler for split from workspace. + List positions = + recentsView.getPagedOrientationHandler().getSplitPositionOptions( + mDeviceProfile); + List> splitShortcuts = new ArrayList<>(); + for (SplitPositionOption position : positions) { + splitShortcuts.add(getSplitSelectShortcutByPosition(position)); + } + return splitShortcuts; } - /** * Recents logic that triggers when launcher state changes or launcher activity stops/resumes. */ @@ -223,53 +390,65 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0; boolean visible = (state == NORMAL || state == OVERVIEW) && (willUserBeActive || isUserActive()) - && !profile.isVerticalBarLayout() - && profile.isPhone && !profile.isLandscape; - UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0, - profile.hotseatBarSizePx); + && !profile.isVerticalBarLayout(); + if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM) { + SystemUiProxy.INSTANCE.get(this) + .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx); + } else { + SystemUiProxy.INSTANCE.get(this).setShelfHeight(visible, profile.hotseatBarSizePx); + } } if (state == NORMAL && !inTransition) { ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false); } } - @Override public void bindExtraContainerItems(FixedContainerItems item) { if (item.containerId == Favorites.CONTAINER_PREDICTION) { mAllAppsPredictions = item; - getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class) - .setPredictedApps(item.items); + PredictionRowView predictionRowView = + getAppsView().getFloatingHeaderView().findFixedRowByType( + PredictionRowView.class); + predictionRowView.setPredictedApps(item.items); } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { mHotseatPredictionController.setPredictedItems(item); } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) { getPopupDataProvider().setRecommendedWidgets(item.items); } } - @Override - public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { + public void bindWorkspaceComponentsRemoved(Predicate matcher) { super.bindWorkspaceComponentsRemoved(matcher); mHotseatPredictionController.onModelItemsRemoved(matcher); } - @Override public void onDestroy() { + mAppTransitionManager.onActivityDestroyed(); + if (mUnfoldTransitionProgressProvider != null) { + mUnfoldTransitionProgressProvider.destroy(); + } + mTISBindHelper.onDestroy(); + if (mTaskbarManager != null) { + mTaskbarManager.clearActivity(this); + } + if (mLauncherUnfoldAnimationController != null) { + mLauncherUnfoldAnimationController.onDestroy(); + } super.onDestroy(); mHotseatPredictionController.destroy(); + mSplitWithKeyboardShortcutController.onDestroy(); + if (mViewCapture != null) mViewCapture.close(); } - @Override public void onStateSetEnd(LauncherState state) { super.onStateSetEnd(state); - + handlePendingActivityRequest(); switch (state.ordinal) { case HINT_STATE_ORDINAL: { - Workspace workspace = getWorkspace(); + Workspace workspace = getWorkspace(); getStateManager().goToState(NORMAL); if (workspace.getNextPage() != Workspace.DEFAULT_PAGE) { workspace.post(workspace::moveToDefaultScreen); - } else { - handleHomeTap(); } break; } @@ -279,13 +458,13 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { break; } case OVERVIEW_STATE_ORDINAL: { - RecentsView rv = getOverviewPanel(); + RecentsView rv = (RecentsView) getOverviewPanel(); sendCustomAccessibilityEvent( rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null); break; } case QUICK_SWITCH_STATE_ORDINAL: { - RecentsView rv = getOverviewPanel(); + RecentsView rv = (RecentsView) getOverviewPanel(); TaskView tasktolaunch = rv.getTaskViewAt(0); if (tasktolaunch != null) { tasktolaunch.launchTask(success -> { @@ -300,14 +479,11 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { } break; } - } } - @Override public TouchController[] createTouchControllers() { - Mode mode = SysUINavigationMode.getMode(this); - + NavigationMode mode = DisplayController.getNavigationMode(this); ArrayList list = new ArrayList<>(); list.add(getDragController()); switch (mode) { @@ -327,57 +503,660 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { default: list.add(new PortraitStatesTouchController(this)); } - if (!getDeviceProfile().isMultiWindowMode) { list.add(new StatusBarTouchController(this)); } - list.add(new LauncherTaskViewController(this)); return list.toArray(new TouchController[list.size()]); } - @Override public AtomicAnimationFactory createAtomicAnimationFactory() { return new QuickstepAtomicAnimationFactory(this); } - - protected LauncherAppWidgetHost createAppWidgetHost() { - LauncherAppWidgetHost appWidgetHost = super.createAppWidgetHost(); - if (ENABLE_QUICKSTEP_WIDGET_APP_START.get() && LawnchairApp.isRecentsEnabled()) { - appWidgetHost.setInteractionHandler(new QuickstepInteractionHandler(this)); - } - return appWidgetHost; + @Override + protected LauncherWidgetHolder createAppWidgetHolder() { + final QuickstepHolderFactory factory = + (QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory.newFactory(this); + return factory.newInstance(this, + appWidgetId -> getWorkspace().removeWidget(appWidgetId), + new QuickstepInteractionHandler(this)); + } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mPendingSplitSelectInfo = ObjectWrapper.unwrap( + savedInstanceState.getIBinder(PENDING_SPLIT_SELECT_INFO)); + } + addMultiWindowModeChangedListener(mDepthController); + initUnfoldTransitionProgressProvider(); + if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { +// mViewCapture = SettingsAwareViewCapture.getInstance(this).startCapture(getWindow()); + } + getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE); + } + @Override + public void startSplitSelection(SplitSelectSource splitSelectSource) { + RecentsView recentsView = getOverviewPanel(); + ComponentKey componentToBeStaged = new ComponentKey( + splitSelectSource.itemInfo.getTargetComponent(), + splitSelectSource.itemInfo.user); + // Check if there is already an instance of this app running, if so, initiate the split + // using that. + mSplitSelectStateController.findLastActiveTaskAndRunCallback( + componentToBeStaged, + foundTask -> { + splitSelectSource.alreadyRunningTaskId = foundTask == null + ? INVALID_TASK_ID + : foundTask.key.id; + if (ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) { + startSplitToHome(splitSelectSource); + } else { + recentsView.initiateSplitSelect(splitSelectSource); + } + } + ); + } + /** TODO(b/266482558) Migrate into SplitSelectStateController or someplace split specific. */ + private void startSplitToHome(SplitSelectSource source) { + AbstractFloatingView.closeAllOpenViews(this); + int splitPlaceholderSize = getResources().getDimensionPixelSize( + R.dimen.split_placeholder_size); + int splitPlaceholderInset = getResources().getDimensionPixelSize( + R.dimen.split_placeholder_inset); + Rect tempRect = new Rect(); + mSplitSelectStateController.setInitialTaskSelect(source.intent, + source.position.stagePosition, source.itemInfo, source.splitEvent, + source.alreadyRunningTaskId); + RecentsView recentsView = getOverviewPanel(); + recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds( + splitPlaceholderSize, splitPlaceholderInset, getDeviceProfile(), + mSplitSelectStateController.getActiveSplitStagePosition(), tempRect); + PendingAnimation anim = new PendingAnimation(TABLET_HOME_TO_SPLIT.getDuration()); + RectF startingTaskRect = new RectF(); + final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(this, + source.getView(), null /* thumbnail */, source.getDrawable(), startingTaskRect); + floatingTaskView.setAlpha(1); + floatingTaskView.addStagingAnimation(anim, startingTaskRect, tempRect, + false /* fadeWithThumbnail */, true /* isStagedTask */); + mSplitSelectStateController.setFirstFloatingTaskView(floatingTaskView); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + getDragLayer().removeView(floatingTaskView); + mSplitSelectStateController.resetState(); + } + }); + anim.buildAnim().start(); + } + @Override + protected void onResume() { + super.onResume(); + if (mLauncherUnfoldAnimationController != null) { + mLauncherUnfoldAnimationController.onResume(); + } + } + @Override + protected void onPause() { + if (mLauncherUnfoldAnimationController != null) { + mLauncherUnfoldAnimationController.onPause(); + } + super.onPause(); + } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (mOverviewCommandHelper != null) { + mOverviewCommandHelper.clearPendingCommands(); + } + } + public QuickstepTransitionManager getAppTransitionManager() { + return mAppTransitionManager; + } + @Override + public void onEnterAnimationComplete() { + super.onEnterAnimationComplete(); + // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled + // as a part of quickstep, so that high-res thumbnails can load the next time we enter + // overview + RecentsModel.INSTANCE.get(this).getThumbnailCache() + .getHighResLoadingState().setVisible(true); + } + @Override + protected void handleGestureContract(Intent intent) { + if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) { + super.handleGestureContract(intent); + } + } + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + RecentsModel.INSTANCE.get(this).onTrimMemory(level); + } + @Override + public void onUiChangedWhileSleeping() { + // Remove the snapshot because the content view may have obvious changes. + UI_HELPER_EXECUTOR.execute( + () -> ActivityManagerWrapper.getInstance().invalidateHomeTaskSnapshot(this)); + } + @Override + protected void onScreenOnChanged(boolean isOn) { + super.onScreenOnChanged(isOn); + if (!isOn) { + RecentsView recentsView = getOverviewPanel(); + recentsView.finishRecentsAnimation(true /* toRecents */, null); + } + } + @Override + public void onAllAppsTransition(float progress) { + super.onAllAppsTransition(progress); + onTaskbarInAppDisplayProgressUpdate(progress, ALL_APPS_PAGE_PROGRESS_INDEX); + } + @Override + public void onWidgetsTransition(float progress) { + super.onWidgetsTransition(progress); + onTaskbarInAppDisplayProgressUpdate(progress, WIDGETS_PAGE_PROGRESS_INDEX); + if (mEnableWidgetDepth) { + getDepthController().widgetDepth.setValue(Utilities.mapToRange( + progress, 0f, 1f, 0f, getDeviceProfile().bottomSheetDepth, EMPHASIZED)); + } + } + @Override + protected void registerBackDispatcher() { + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + new OnBackAnimationCallback() { + @Nullable OnBackPressedHandler mActiveOnBackPressedHandler; + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + if (mActiveOnBackPressedHandler != null) { + mActiveOnBackPressedHandler.onBackCancelled(); + } + mActiveOnBackPressedHandler = getOnBackPressedHandler(); + mActiveOnBackPressedHandler.onBackStarted(); + } + @Override + public void onBackInvoked() { + // Recreate mActiveOnBackPressedHandler if necessary to avoid NPE because: + // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not + // called on ACTION_DOWN before onBackInvoked() is called in ACTION_UP. + // 2. Launcher#onBackPressed() will call onBackInvoked() without calling + // onBackInvoked() beforehand. + if (mActiveOnBackPressedHandler == null) { + mActiveOnBackPressedHandler = getOnBackPressedHandler(); + } + mActiveOnBackPressedHandler.onBackInvoked(); + mActiveOnBackPressedHandler = null; + TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onBackInvoked"); + } + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + if (!FeatureFlags.IS_STUDIO_BUILD && mActiveOnBackPressedHandler == null) { + return; + } + mActiveOnBackPressedHandler + .onBackProgressed(backEvent.getProgress()); + } + @Override + public void onBackCancelled() { + if (!FeatureFlags.IS_STUDIO_BUILD && mActiveOnBackPressedHandler == null) { + return; + } + mActiveOnBackPressedHandler.onBackCancelled(); + mActiveOnBackPressedHandler = null; + } + }); + } + private void onTaskbarInAppDisplayProgressUpdate(float progress, int flag) { + if (mTaskbarManager == null + || mTaskbarManager.getCurrentActivityContext() == null + || mTaskbarUIController == null) { + return; + } + mTaskbarUIController.onTaskbarInAppDisplayProgressUpdate(progress, flag); + } + @Override + public void startIntentSenderForResult(IntentSender intent, int requestCode, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { + if (requestCode != -1) { + mPendingActivityRequestCode = requestCode; + StartActivityParams params = new StartActivityParams(this, requestCode); + params.intentSender = intent; + params.fillInIntent = fillInIntent; + params.flagsMask = flagsMask; + params.flagsValues = flagsValues; + params.extraFlags = extraFlags; + params.options = options; + startActivity(ProxyActivityStarter.getLaunchIntent(this, params)); + } else { + super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, + flagsValues, extraFlags, options); + } + } + @Override + public void startActivityForResult(Intent intent, int requestCode, Bundle options) { + if (requestCode != -1) { + mPendingActivityRequestCode = requestCode; + StartActivityParams params = new StartActivityParams(this, requestCode); + params.intent = intent; + params.options = options; + startActivity(ProxyActivityStarter.getLaunchIntent(this, params)); + } else { + super.startActivityForResult(intent, requestCode, options); + } + } + @Override + public void setResumed() { + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + DesktopVisibilityController controller = mDesktopVisibilityController; + if (controller != null && controller.areFreeformTasksVisible() + && !controller.isGestureInProgress()) { + // Return early to skip setting activity to appear as resumed + // TODO(b/255649902): shouldn't be needed when we have a separate launcher state + // for desktop that we can use to control other parts of launcher + return; + } + } + super.setResumed(); + } + @Override + protected void onDeferredResumed() { + super.onDeferredResumed(); + handlePendingActivityRequest(); + } + private void handlePendingActivityRequest() { + if (mPendingActivityRequestCode != -1 && isInState(NORMAL) + && ((getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) { + // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher. + onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null); + // ProxyActivityStarter is started with clear task to reset the task after which it + // removes the task itself. + startActivity(ProxyActivityStarter.getLaunchIntent(this, null)); + } + } + private void onTISConnected(TISBinder binder) { + mTaskbarManager = binder.getTaskbarManager(); + mTaskbarManager.setActivity(this); + mOverviewCommandHelper = binder.getOverviewCommandHelper(); + } + @Override + public void runOnBindToTouchInteractionService(Runnable r) { + mTISBindHelper.runOnBindToTouchInteractionService(r); + } + private void initUnfoldTransitionProgressProvider() { + final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig(); + if (config.isEnabled()) { + if (RECEIVE_UNFOLD_EVENTS_FROM_SYSUI.get()) { + initRemotelyCalculatedUnfoldAnimation(config); + } else { + initLocallyCalculatedUnfoldAnimation(config); + } + } + } + /** Registers hinge angle listener and calculates the animation progress in this process. */ + private void initLocallyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) { + UnfoldSharedComponent unfoldComponent = + UnfoldTransitionFactory.createUnfoldSharedComponent( + /* context= */ this, + config, + ProxyScreenStatusProvider.INSTANCE, + new DeviceStateManagerFoldProvider( + getSystemService(DeviceStateManager.class), /* context= */ this), + new ActivityManagerActivityTypeProvider( + getSystemService(ActivityManager.class)), + getSystemService(SensorManager.class), + getMainThreadHandler(), + getMainExecutor(), + /* backgroundExecutor= */ UI_HELPER_EXECUTOR, + /* tracingTagPrefix= */ "launcher", + getSystemService(DisplayManager.class) + ); + mUnfoldTransitionProgressProvider = unfoldComponent.getUnfoldTransitionProvider() + .orElseThrow(() -> new IllegalStateException( + "Trying to create UnfoldTransitionProgressProvider when the " + + "transition is disabled")); + initUnfoldAnimationController(mUnfoldTransitionProgressProvider, + unfoldComponent.getRotationChangeProvider()); + } + /** Receives animation progress from sysui process. */ + private void initRemotelyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) { + RemoteUnfoldSharedComponent unfoldComponent = + UnfoldTransitionFactory.createRemoteUnfoldSharedComponent( + /* context= */ this, + config, + getMainExecutor(), + getMainThreadHandler(), + /* backgroundExecutor= */ UI_HELPER_EXECUTOR, + /* tracingTagPrefix= */ "launcher", + getSystemService(DisplayManager.class) + ); + final RemoteUnfoldTransitionReceiver remoteUnfoldTransitionProgressProvider = + unfoldComponent.getRemoteTransitionProgress().orElseThrow( + () -> new IllegalStateException( + "Trying to create getRemoteTransitionProgress when the transition " + + "is disabled")); + mUnfoldTransitionProgressProvider = remoteUnfoldTransitionProgressProvider; + SystemUiProxy.INSTANCE.get(this).setUnfoldAnimationListener( + remoteUnfoldTransitionProgressProvider); + initUnfoldAnimationController(mUnfoldTransitionProgressProvider, + unfoldComponent.getRotationChangeProvider()); + } + private void initUnfoldAnimationController(UnfoldTransitionProgressProvider progressProvider, + RotationChangeProvider rotationChangeProvider) { + mLauncherUnfoldAnimationController = new LauncherUnfoldAnimationController( + /* launcher= */ this, + getWindowManager(), + progressProvider, + rotationChangeProvider + ); + } + public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) { + mTaskbarUIController = taskbarUIController; + } + public @Nullable LauncherTaskbarUIController getTaskbarUIController() { + return mTaskbarUIController; + } + public SplitSelectStateController getSplitSelectStateController() { + return mSplitSelectStateController; + } + public T getActionsView() { + return (T) mActionsView; + } + @Override + protected void closeOpenViews(boolean animate) { + super.closeOpenViews(animate); + TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); + } + @Override + protected void collectStateHandlers(List out) { + super.collectStateHandlers(out); + out.add(getDepthController()); + out.add(new RecentsViewStateController(this)); + } + public DepthController getDepthController() { + return mDepthController; + } + public DesktopVisibilityController getDesktopVisibilityController() { + return mDesktopVisibilityController; + } + @Nullable + public UnfoldTransitionProgressProvider getUnfoldTransitionProgressProvider() { + return mUnfoldTransitionProgressProvider; + } + @Override + public boolean supportsAdaptiveIconAnimation(View clickedView) { + return mAppTransitionManager.hasControlRemoteAppTransitionPermission(); + } + @Override + public DragOptions getDefaultWorkspaceDragOptions() { + if (mNextWorkspaceDragOptions != null) { + DragOptions options = mNextWorkspaceDragOptions; + mNextWorkspaceDragOptions = null; + return options; + } + return super.getDefaultWorkspaceDragOptions(); + } + public void setNextWorkspaceDragOptions(DragOptions dragOptions) { + mNextWorkspaceDragOptions = dragOptions; + } + @Override + public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { + QuickstepTransitionManager appTransitionManager = getAppTransitionManager(); + appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() { + @Override + public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets) { + // On the first call clear the reference. + signal.cancel(); + ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0); + fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets, + wallpaperTargets)); + AnimatorSet anim = new AnimatorSet(); + anim.play(fadeAnimation); + return anim; + } + }, signal); + } + @Override + public float[] getNormalOverviewScaleAndOffset() { + return DisplayController.getNavigationMode(this).hasGestures + ? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET}; + } + @Override + public void finishBindingItems(IntSet pagesBoundFirst) { + super.finishBindingItems(pagesBoundFirst); + // Instantiate and initialize WellbeingModel now that its loading won't interfere with + // populating workspace. + // TODO: Find a better place for this + WellbeingModel.INSTANCE.get(this); + } + @Override + public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) { + pendingTasks.add(() -> { + // This is added in pending task as we need to wait for views to be positioned + // correctly before registering them for the animation. + if (mLauncherUnfoldAnimationController != null) { + // This is needed in case items are rebound while the unfold animation is in + // progress. + mLauncherUnfoldAnimationController.updateRegisteredViewsIfNeeded(); + } + }); + super.onInitialBindComplete(boundPages, pendingTasks); + } + @Override + public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { + ActivityOptionsWrapper activityOptions = + mAppTransitionManager.hasControlRemoteAppTransitionPermission() + ? mAppTransitionManager.getActivityLaunchOptions(v) + : super.getActivityLaunchOptions(v, item); + if (mLastTouchUpTime > 0) { + activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER, + mLastTouchUpTime); + } + if (item != null && (item.animationType == DEFAULT_NO_ICON + || item.animationType == VIEW_BACKGROUND)) { + activityOptions.options.setSplashScreenStyle( + SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR); + } else { + activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); + } + activityOptions.options.setLaunchDisplayId( + (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId() + : Display.DEFAULT_DISPLAY); + addLaunchCookie(item, activityOptions.options); + return activityOptions; + } + @Override + @BinderThread + public void enterStageSplitFromRunningApp(boolean leftOrTop) { + 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: + // 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_SHORTCUT: + 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); + // When changing screens, force moving to rest state similar to StatefulActivity.onStop, as + // StatefulActivity isn't called consistently. + if ((flags & CHANGE_ACTIVE_SCREEN) != 0) { + // Do not animate moving to rest state, as it can clash with Launcher#onIdpChanged + // where reapplyUi calls StateManager's reapplyState during the state change animation, + // and cancel the state change unexpectedly. The screen will be off during screen + // transition, hiding the unanimated transition. + getStateManager().moveToRestState(/* isAnimated = */false); + } + if ((flags & CHANGE_NAVIGATION_MODE) != 0) { + getDragLayer().recreateControllers(); + if (mActionsView != null) { + mActionsView.updateVerticalMargin(info.navigationMode); + } + } + } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // If Launcher shuts downs during split select, we save some extra data in the recovery + // bundle to allow graceful recovery. The normal LauncherState restore mechanism doesn't + // work in this case because restoring straight to OverviewSplitSelect without staging data, + // or before the tasks themselves have loaded into Overview, causes a crash. So we tell + // Launcher to first restore into Overview state, wait for the relevant tasks and icons to + // load in, and then proceed to OverviewSplitSelect. + if (isInState(OVERVIEW_SPLIT_SELECT)) { + // Launcher will restart in Overview and then transition to OverviewSplitSelect. + outState.putIBinder(PENDING_SPLIT_SELECT_INFO, ObjectWrapper.wrap( + new PendingSplitSelectInfo( + mSplitSelectStateController.getInitialTaskId(), + mSplitSelectStateController.getActiveSplitStagePosition(), + mSplitSelectStateController.getSplitEvent()) + )); + outState.putInt(RUNTIME_STATE, OVERVIEW.ordinal); + } + } + /** + * When Launcher restarts, it sometimes needs to recover to a split selection state. + * This function checks if such a recovery is needed. + * @return a boolean representing whether the launcher is waiting to recover to + * OverviewSplitSelect state. + */ + public boolean hasPendingSplitSelectInfo() { + return mPendingSplitSelectInfo != null; + } + /** + * See {@link #hasPendingSplitSelectInfo()} + */ + public @Nullable PendingSplitSelectInfo getPendingSplitSelectInfo() { + return mPendingSplitSelectInfo; + } + /** + * When the launcher has successfully recovered to OverviewSplitSelect state, this function + * deletes the recovery data, returning it to a null state. + */ + public void finishSplitSelectRecovery() { + mPendingSplitSelectInfo = null; + } + @Override + public boolean areFreeformTasksVisible() { + if (mDesktopVisibilityController != null) { + return mDesktopVisibilityController.areFreeformTasksVisible(); + } + return false; + } + @Override + protected void onDeviceProfileInitiated() { + super.onDeviceProfileInitiated(); + SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx); + } + @Override + public void dispatchDeviceProfileChanged() { + super.dispatchDeviceProfileChanged(); + Trace.instantForTrack(TRACE_TAG_APP, "QuickstepLauncher#DeviceProfileChanged", + getDeviceProfile().toSmallString()); + SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx); + if (mTaskbarManager != null) { + mTaskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged"); + } + } + /** + * Launches the given {@link GroupTask} in splitscreen. + * + * If the second split task is missing, launches the first task normally. + */ + public void launchSplitTasks(@NonNull View taskView, @NonNull GroupTask groupTask) { + if (groupTask.task2 == null) { + UI_HELPER_EXECUTOR.execute(() -> + ActivityManagerWrapper.getInstance().startActivityFromRecents( + groupTask.task1.key, + getActivityLaunchOptions(taskView, null).options)); + return; + } + mSplitSelectStateController.launchTasks( + groupTask.task1.key.id, + groupTask.task2.key.id, + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, + /* callback= */ success -> {}, + /* freezeTaskList= */ true, + groupTask.mSplitBounds == null + ? DEFAULT_SPLIT_RATIO + : groupTask.mSplitBounds.appsStackedVertically + ? groupTask.mSplitBounds.topTaskPercent + : groupTask.mSplitBounds.leftTaskPercent); } - private static final class LauncherTaskViewController extends TaskViewTouchController { - LauncherTaskViewController(Launcher activity) { super(activity); } - @Override protected boolean isRecentsInteractive() { return mActivity.isInState(OVERVIEW) || mActivity.isInState(OVERVIEW_MODAL_TASK); } - @Override protected boolean isRecentsModal() { return mActivity.isInState(OVERVIEW_MODAL_TASK); } - @Override protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { mActivity.getStateManager().setCurrentUserControlledAnimation(animController); } } - @Override public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { super.dump(prefix, fd, writer, args); + if (mDepthController != null) { + mDepthController.dump(prefix, writer); + } RecentsView recentsView = getOverviewPanel(); writer.println("\nQuickstepLauncher:"); writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" : recentsView.getPagedViewOrientedState())); } -} +} \ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java new file mode 100644 index 0000000000..b318100205 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java @@ -0,0 +1,365 @@ +/** + * 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.uioverrides; + +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; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.util.Log; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; + +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.IntSet; +import com.android.launcher3.widget.LauncherAppWidgetHostView; +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.LauncherWidgetHolder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.function.BiConsumer; +import java.util.function.IntConsumer; + +/** + * {@link LauncherWidgetHolder} that puts the app widget host in the background + */ +public final class QuickstepWidgetHolder extends LauncherWidgetHolder { + + private static final String TAG = "QuickstepWidgetHolder"; + + private static final UpdateKey KEY_PROVIDER_UPDATE = + AppWidgetHostView::onUpdateProviderInfo; + private static final UpdateKey KEY_VIEWS_UPDATE = + AppWidgetHostView::updateAppWidget; + private static final UpdateKey KEY_VIEW_DATA_CHANGED = + AppWidgetHostView::onViewDataChanged; + + private static final List sHolders = new ArrayList<>(); + private static final SparseArray sListeners = + new SparseArray<>(); + + private static AppWidgetHost sWidgetHost = null; + + private final SparseArray mViews = new SparseArray<>(); + + private final @Nullable RemoteViews.InteractionHandler mInteractionHandler; + + private final @NonNull IntConsumer mAppWidgetRemovedCallback; + + private final ArrayList mProviderChangedListeners = new ArrayList<>(); + // Map to all pending updated keyed with appWidgetId; + private final SparseArray mPendingUpdateMap = new SparseArray<>(); + + private QuickstepWidgetHolder(@NonNull Context context, + @Nullable IntConsumer appWidgetRemovedCallback, + @Nullable RemoteViews.InteractionHandler interactionHandler) { + super(context, appWidgetRemovedCallback); + mAppWidgetRemovedCallback = appWidgetRemovedCallback != null ? appWidgetRemovedCallback + : i -> {}; + mInteractionHandler = interactionHandler; + MAIN_EXECUTOR.execute(() -> sHolders.add(this)); + } + + @Override + @NonNull + protected AppWidgetHost createHost(@NonNull Context context, + @Nullable IntConsumer appWidgetRemovedCallback) { + if (sWidgetHost == null) { + sWidgetHost = new QuickstepAppWidgetHost(context.getApplicationContext(), + i -> MAIN_EXECUTOR.execute(() -> + sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))), + () -> MAIN_EXECUTOR.execute(() -> + sHolders.forEach(h -> h.mProviderChangedListeners.forEach( + ProviderChangedListener::notifyWidgetProvidersChanged))), + UI_HELPER_EXECUTOR.getLooper()); + if (!WidgetsModel.GO_DISABLE_WIDGETS) { + sWidgetHost.startListening(); + } + } + return sWidgetHost; + } + + @Override + protected void updateDeferredView() { + super.updateDeferredView(); + int count = mPendingUpdateMap.size(); + for (int i = 0; i < count; i++) { + int widgetId = mPendingUpdateMap.keyAt(i); + AppWidgetHostView view = mViews.get(widgetId); + if (view == null) { + continue; + } + PendingUpdate pendingUpdate = mPendingUpdateMap.valueAt(i); + if (pendingUpdate == null) { + continue; + } + if (pendingUpdate.providerInfo != null) { + KEY_PROVIDER_UPDATE.accept(view, pendingUpdate.providerInfo); + } + if (pendingUpdate.remoteViews != null) { + KEY_VIEWS_UPDATE.accept(view, pendingUpdate.remoteViews); + } + pendingUpdate.changedViews.forEach( + viewId -> KEY_VIEW_DATA_CHANGED.accept(view, viewId)); + } + mPendingUpdateMap.clear(); + } + + private void onWidgetUpdate(int widgetId, UpdateKey key, T data) { + if (isListening()) { + AppWidgetHostView view = mViews.get(widgetId); + if (view == null) { + return; + } + key.accept(view, data); + return; + } + + PendingUpdate pendingUpdate = mPendingUpdateMap.get(widgetId); + if (pendingUpdate == null) { + pendingUpdate = new PendingUpdate(); + mPendingUpdateMap.put(widgetId, pendingUpdate); + } + + if (KEY_PROVIDER_UPDATE.equals(key)) { + // For provider change, remove all updates + pendingUpdate.providerInfo = (AppWidgetProviderInfo) data; + pendingUpdate.remoteViews = null; + pendingUpdate.changedViews.clear(); + } else if (KEY_VIEWS_UPDATE.equals(key)) { + // For views update, remove all previous updates, except the provider + pendingUpdate.remoteViews = (RemoteViews) data; + pendingUpdate.changedViews.clear(); + } else if (KEY_VIEW_DATA_CHANGED.equals(key)) { + pendingUpdate.changedViews.add((Integer) data); + } + } + + /** + * Delete the specified app widget from the host + * @param appWidgetId The ID of the app widget to be deleted + */ + @Override + public void deleteAppWidgetId(int appWidgetId) { + super.deleteAppWidgetId(appWidgetId); + mViews.remove(appWidgetId); + sListeners.remove(appWidgetId); + } + + /** + * Called when the launcher is destroyed + */ + @Override + public void destroy() { + try { + MAIN_EXECUTOR.submit(() -> sHolders.remove(this)).get(); + } catch (Exception e) { + Log.e(TAG, "Failed to remove self from holder list", e); + } + } + + @Override + protected boolean shouldListen(int flags) { + return (flags & (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED)) + == (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED); + } + + /** + * Add a listener that is triggered when the providers of the widgets are changed + * @param listener The listener that notifies when the providers changed + */ + @Override + public void addProviderChangeListener( + @NonNull LauncherWidgetHolder.ProviderChangedListener listener) { + mProviderChangedListeners.add(listener); + } + + /** + * Remove the specified listener from the host + * @param listener The listener that is to be removed from the host + */ + @Override + public void removeProviderChangeListener( + LauncherWidgetHolder.ProviderChangedListener listener) { + mProviderChangedListeners.remove(listener); + } + + /** + * Stop the host from updating the widget views + */ + @Override + public void stopListening() { + if (WidgetsModel.GO_DISABLE_WIDGETS) { + return; + } + + sWidgetHost.setAppWidgetHidden(); + setListeningFlag(false); + } + + /** + * Create a view for the specified app widget + * @param context The activity context for which the view is created + * @param appWidgetId The ID of the widget + * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget + * @return A view for the widget + */ + @NonNull + @Override + public LauncherAppWidgetHostView createView(@NonNull Context context, int appWidgetId, + @NonNull LauncherAppWidgetProviderInfo appWidget) { + LauncherAppWidgetHostView widgetView = getPendingView(appWidgetId); + if (widgetView != null) { + removePendingView(appWidgetId); + } else { + widgetView = new LauncherAppWidgetHostView(context); + } + widgetView.setInteractionHandler(mInteractionHandler); + widgetView.setAppWidget(appWidgetId, appWidget); + mViews.put(appWidgetId, widgetView); + + QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId); + if (listener == null) { + listener = new QuickstepWidgetHolderListener(appWidgetId); + sWidgetHost.setListener(appWidgetId, listener); + sListeners.put(appWidgetId, listener); + } + RemoteViews remoteViews = listener.addHolder(this); + widgetView.updateAppWidget(remoteViews); + + return widgetView; + } + + /** + * Clears all the views from the host + */ + @Override + public void clearViews() { + mViews.clear(); + for (int i = sListeners.size() - 1; i >= 0; i--) { + sListeners.valueAt(i).mListeningHolders.remove(this); + } + } + + private static class QuickstepWidgetHolderListener + implements AppWidgetHost.AppWidgetHostListener { + + // Static listeners should use a set that is backed by WeakHashMap to avoid memory leak + private final Set mListeningHolders = Collections.newSetFromMap( + new WeakHashMap<>()); + + private final int mWidgetId; + + private @Nullable RemoteViews mRemoteViews; + + QuickstepWidgetHolderListener(int widgetId) { + mWidgetId = widgetId; + } + + @UiThread + @Nullable + public RemoteViews addHolder(@NonNull QuickstepWidgetHolder holder) { + mListeningHolders.add(holder); + return mRemoteViews; + } + + @Override + @WorkerThread + public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) { + mRemoteViews = null; + executeOnMainExecutor(KEY_PROVIDER_UPDATE, info); + } + + @Override + @WorkerThread + public void updateAppWidget(@Nullable RemoteViews views) { + mRemoteViews = views; + executeOnMainExecutor(KEY_VIEWS_UPDATE, mRemoteViews); + } + + @Override + @WorkerThread + public void onViewDataChanged(int viewId) { + executeOnMainExecutor(KEY_VIEW_DATA_CHANGED, viewId); + } + + private void executeOnMainExecutor(UpdateKey key, T data) { + MAIN_EXECUTOR.execute(() -> mListeningHolders.forEach(holder -> + holder.onWidgetUpdate(mWidgetId, key, data))); + } + } + + /** + * {@code HolderFactory} subclass that takes an interaction handler as one of the parameters + * when creating a new instance. + */ + public static class QuickstepHolderFactory extends HolderFactory { + + @SuppressWarnings("unused") + public QuickstepHolderFactory(Context context) { } + + @Override + public LauncherWidgetHolder newInstance(@NonNull Context context, + @Nullable IntConsumer appWidgetRemovedCallback) { + return newInstance(context, appWidgetRemovedCallback, null); + } + + /** + * @param context The context of the caller + * @param appWidgetRemovedCallback The callback that is called when widgets are removed + * @param interactionHandler The interaction handler when the widgets are clicked + * @return A new {@link LauncherWidgetHolder} instance + */ + public LauncherWidgetHolder newInstance(@NonNull Context context, + @Nullable IntConsumer appWidgetRemovedCallback, + @Nullable RemoteViews.InteractionHandler interactionHandler) { + + if (!FeatureFlags.ENABLE_WIDGET_HOST_IN_BACKGROUND.get()) { + return new LauncherWidgetHolder(context, appWidgetRemovedCallback) { + @Override + protected AppWidgetHost createHost(Context context, + @Nullable IntConsumer appWidgetRemovedCallback) { + AppWidgetHost host = super.createHost(context, appWidgetRemovedCallback); + host.setInteractionHandler(interactionHandler); + return host; + } + }; + } + return new QuickstepWidgetHolder(context, appWidgetRemovedCallback, interactionHandler); + } + } + + private static class PendingUpdate { + public final IntSet changedViews = new IntSet(); + public AppWidgetProviderInfo providerInfo; + public RemoteViews remoteViews; + } + + private interface UpdateKey extends BiConsumer { } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java index 32ce1c40d7..f16b43df5e 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java @@ -20,6 +20,7 @@ import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS; import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE; +import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; import static com.android.quickstep.views.RecentsView.TASK_MODALNESS; @@ -27,22 +28,24 @@ import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLA import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION; import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL; +import android.animation.AnimatorSet; import android.annotation.TargetApi; import android.os.Build; import android.util.FloatProperty; +import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.PagedOrientationHandler; -import com.android.launcher3.util.MultiValueAlpha; +import com.android.quickstep.util.AnimUtils; +import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.views.ClearAllButton; import com.android.quickstep.views.LauncherRecentsView; import com.android.quickstep.views.RecentsView; @@ -55,7 +58,7 @@ import com.android.quickstep.views.RecentsView; public final class RecentsViewStateController extends BaseRecentsViewStateController { - public RecentsViewStateController(BaseQuickstepLauncher launcher) { + public RecentsViewStateController(QuickstepLauncher launcher) { super(launcher); } @@ -72,7 +75,10 @@ public final class RecentsViewStateController extends // DepthController to prevent optimizations which might occlude the layers behind mLauncher.getDepthController().setHasContentBehindLauncher(state.overviewUi); - handleSplitSelectionState(state, null); + PendingAnimation builder = + new PendingAnimation(state.getTransitionDuration(mLauncher, true)); + + handleSplitSelectionState(state, builder, /* animate */false); } @Override @@ -84,6 +90,13 @@ public final class RecentsViewStateController extends // While animating into recents, update the visible task data as needed builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL)); mRecentsView.updateEmptyMessage(); + // TODO(b/246283207): Remove logging once root cause of flake detected. + if (Utilities.isRunningInTestHarness()) { + Log.d("b/246283207", "RecentsView#setStateWithAnimationInternal getCurrentPage(): " + + mRecentsView.getCurrentPage() + + ", getScrollForPage(getCurrentPage())): " + + mRecentsView.getScrollForPage(mRecentsView.getCurrentPage())); + } } else { builder.addListener( AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals)); @@ -93,7 +106,7 @@ public final class RecentsViewStateController extends builder.addListener(AnimatorListeners.forSuccessCallback(() -> mLauncher.getDepthController().setHasContentBehindLauncher(toState.overviewUi))); - handleSplitSelectionState(toState, builder); + handleSplitSelectionState(toState, builder, /* animate */true); setAlphas(builder, config, toState); builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS, @@ -106,9 +119,13 @@ public final class RecentsViewStateController extends * will add animations to builder. */ private void handleSplitSelectionState(@NonNull LauncherState toState, - @Nullable PendingAnimation builder) { - LauncherState currentState = mLauncher.getStateManager().getState(); - boolean animate = builder != null; + @NonNull PendingAnimation builder, boolean animate) { + if (toState != OVERVIEW_SPLIT_SELECT) { + // Not going to split + return; + } + + // Create transition animations to split select PagedOrientationHandler orientationHandler = ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler(); Pair taskViewsFloat = @@ -116,37 +133,25 @@ public final class RecentsViewStateController extends TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, mLauncher.getDeviceProfile()); - if (isSplitSelectionState(currentState, toState)) { - // Animation to "dismiss" selected taskView - PendingAnimation splitSelectInitAnimation = - mRecentsView.createSplitSelectInitAnimation(); - // Add properties to shift remaining taskViews to get out of placeholder view - splitSelectInitAnimation.setFloat(mRecentsView, taskViewsFloat.first, - toState.getSplitSelectTranslation(mLauncher), LINEAR); - splitSelectInitAnimation.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR); + SplitAnimationTimings timings = + AnimUtils.getDeviceOverviewToSplitTimings(mLauncher.getDeviceProfile().isTablet); - if (!animate && isSplitSelectionState(currentState, toState)) { - splitSelectInitAnimation.buildAnim().start(); - } else if (animate && - isSplitSelectionState(currentState, toState)) { - builder.add(splitSelectInitAnimation.buildAnim()); - } + mRecentsView.createSplitSelectInitAnimation(builder, + toState.getTransitionDuration(mLauncher, true /* isToState */)); + // Shift tasks vertically downward to get out of placeholder view + builder.setFloat(mRecentsView, taskViewsFloat.first, + toState.getSplitSelectTranslation(mLauncher), + timings.getGridSlidePrimaryInterpolator()); + // Zero out horizontal translation + builder.setFloat(mRecentsView, taskViewsFloat.second, + 0, + timings.getGridSlideSecondaryInterpolator()); + + if (!animate) { + AnimatorSet as = builder.buildAnim(); + as.start(); + as.end(); } - - if (isSplitSelectionState(currentState, toState)) { - mRecentsView.applySplitPrimaryScrollOffset(); - } else { - mRecentsView.resetSplitPrimaryScrollOffset(); - } - } - - /** - * @return true if {@param toState} is {@link LauncherState#OVERVIEW_SPLIT_SELECT} - * and {@param fromState} is not {@link LauncherState#OVERVIEW_SPLIT_SELECT} - */ - private boolean isSplitSelectionState(@NonNull LauncherState fromState, - @NonNull LauncherState toState) { - return fromState != OVERVIEW_SPLIT_SELECT && toState == OVERVIEW_SPLIT_SELECT; } private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config, @@ -156,7 +161,7 @@ public final class RecentsViewStateController extends clearAllButtonAlpha, LINEAR); float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0; propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(), - MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator( + MULTI_PROPERTY_VALUE, overviewButtonAlpha, config.getInterpolator( ANIM_OVERVIEW_ACTIONS_FADE, LINEAR)); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DebugFlag.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DebugFlag.java new file mode 100644 index 0000000000..d4944d0315 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DebugFlag.java @@ -0,0 +1,38 @@ +/* + * 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.uioverrides.flags; + +import com.android.launcher3.config.FeatureFlags.BooleanFlag; + +class DebugFlag extends BooleanFlag { + + public final String key; + public final String description; + + public final boolean defaultValue; + + public DebugFlag(String key, String description, boolean defaultValue, boolean currentValue) { + super(currentValue); + this.key = key; + this.defaultValue = defaultValue; + this.description = description; + } + + @Override + public String toString() { + return key + ": defaultValue=" + defaultValue + ", mCurrentValue=" + get(); + } +} diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java similarity index 84% rename from src/com/android/launcher3/settings/DeveloperOptionsFragment.java rename to quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java index b06b8a10bf..67ea1af056 100644 --- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.settings; +package com.android.launcher3.uioverrides.flags; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_CHANGED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.pm.PackageManager.GET_RESOLVED_FILTER; import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; import static android.view.View.GONE; @@ -25,11 +28,9 @@ import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLU import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey; import android.annotation.TargetApi; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -59,12 +60,13 @@ import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.preference.SwitchPreference; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.FlagTogglerPrefUi; +import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.OnboardingPrefs; +import com.android.launcher3.util.SimpleBroadcastReceiver; import java.util.ArrayList; import java.util.List; @@ -82,12 +84,8 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS"; private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; - private final BroadcastReceiver mPluginReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - loadPluginPrefs(); - } - }; + private final SimpleBroadcastReceiver mPluginReceiver = + new SimpleBroadcastReceiver(i -> loadPluginPrefs()); private PreferenceScreen mPreferenceScreen; @@ -96,13 +94,9 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - getContext().registerReceiver(mPluginReceiver, filter); - getContext().registerReceiver(mPluginReceiver, - new IntentFilter(Intent.ACTION_USER_UNLOCKED)); + mPluginReceiver.registerPkgActions(getContext(), null, + ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED); + mPluginReceiver.register(getContext(), Intent.ACTION_USER_UNLOCKED); mPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext()); setPreferenceScreen(mPreferenceScreen); @@ -184,7 +178,7 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { @Override public void onDestroy() { super.onDestroy(); - getContext().unregisterReceiver(mPluginReceiver); + mPluginReceiver.unregisterReceiverSafely(getContext()); } private PreferenceCategory newCategory(String title) { @@ -300,17 +294,27 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { } PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox"); sandboxCategory.setSummary("Learn and practice navigation gestures"); + Preference launchTutorialStepMenuPreference = new Preference(context); + launchTutorialStepMenuPreference.setKey("launchTutorialStepMenu"); + launchTutorialStepMenuPreference.setTitle("Launch Gesture Tutorial Steps menu"); + launchTutorialStepMenuPreference.setSummary("Select a gesture tutorial step."); + launchTutorialStepMenuPreference.setOnPreferenceClickListener(preference -> { + startActivity(launchSandboxIntent.putExtra("use_tutorial_menu", true)); + return true; + }); + sandboxCategory.addPreference(launchTutorialStepMenuPreference); Preference launchOnboardingTutorialPreference = new Preference(context); launchOnboardingTutorialPreference.setKey("launchOnboardingTutorial"); launchOnboardingTutorialPreference.setTitle("Launch Onboarding Tutorial"); launchOnboardingTutorialPreference.setSummary("Learn the basic navigation gestures."); launchOnboardingTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] { - "HOME_NAVIGATION", - "BACK_NAVIGATION", - "OVERVIEW_NAVIGATION"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", + new String[] { + "HOME_NAVIGATION", + "BACK_NAVIGATION", + "OVERVIEW_NAVIGATION"})); return true; }); sandboxCategory.addPreference(launchOnboardingTutorialPreference); @@ -319,9 +323,9 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { launchBackTutorialPreference.setTitle("Launch Back Tutorial"); launchBackTutorialPreference.setSummary("Learn how to use the Back gesture"); launchBackTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] {"BACK_NAVIGATION"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", new String[] {"BACK_NAVIGATION"})); return true; }); sandboxCategory.addPreference(launchBackTutorialPreference); @@ -330,9 +334,9 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { launchHomeTutorialPreference.setTitle("Launch Home Tutorial"); launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture"); launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] {"HOME_NAVIGATION"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", new String[] {"HOME_NAVIGATION"})); return true; }); sandboxCategory.addPreference(launchHomeTutorialPreference); @@ -341,9 +345,9 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial"); launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture"); launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] {"OVERVIEW_NAVIGATION"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", new String[] {"OVERVIEW_NAVIGATION"})); return true; }); sandboxCategory.addPreference(launchOverviewTutorialPreference); @@ -352,9 +356,9 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial"); launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture"); launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] {"ASSISTANT"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", new String[] {"ASSISTANT"})); return true; }); sandboxCategory.addPreference(launchAssistantTutorialPreference); @@ -363,12 +367,22 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { launchSandboxModeTutorialPreference.setTitle("Launch Sandbox Mode"); launchSandboxModeTutorialPreference.setSummary("Practice navigation gestures"); launchSandboxModeTutorialPreference.setOnPreferenceClickListener(preference -> { - startActivity(launchSandboxIntent.putExtra( - "tutorial_steps", - new String[] {"SANDBOX_MODE"})); + startActivity(launchSandboxIntent + .putExtra("use_tutorial_menu", false) + .putExtra("tutorial_steps", new String[] {"SANDBOX_MODE"})); return true; }); sandboxCategory.addPreference(launchSandboxModeTutorialPreference); + + Preference launchSecondaryDisplayPreference = new Preference(context); + launchSecondaryDisplayPreference.setKey("launchSecondaryDisplay"); + launchSecondaryDisplayPreference.setTitle("Launch Secondary Display"); + launchSecondaryDisplayPreference.setSummary("Launch secondary display activity"); + launchSecondaryDisplayPreference.setOnPreferenceClickListener(preference -> { + startActivity(new Intent(context, SecondaryDisplayLauncher.class)); + return true; + }); + sandboxCategory.addPreference(launchSecondaryDisplayPreference); } private void addOnboardingPrefsCatergory() { @@ -381,7 +395,8 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { onboardingPref.setTitle(title); onboardingPref.setSummary("Tap to reset"); onboardingPref.setOnPreferenceClickListener(preference -> { - SharedPreferences.Editor sharedPrefsEdit = Utilities.getPrefs(getContext()).edit(); + SharedPreferences.Editor sharedPrefsEdit = LauncherPrefs.getPrefs(getContext()) + .edit(); for (String key : keys) { sharedPrefsEdit.remove(key); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeviceFlag.java new file mode 100644 index 0000000000..3900ebb549 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeviceFlag.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides.flags; + +class DeviceFlag extends DebugFlag { + + private final boolean mDefaultValueInCode; + + public DeviceFlag(String key, String description, boolean defaultValue, + boolean currentValue, boolean defaultValueInCode) { + super(key, description, defaultValue, currentValue); + mDefaultValueInCode = defaultValueInCode; + } + + @Override + public String toString() { + return super.toString() + ", mDefaultValueInCode=" + mDefaultValueInCode; + } +} diff --git a/src/com/android/launcher3/config/FlagTogglerPrefUi.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java similarity index 82% rename from src/com/android/launcher3/config/FlagTogglerPrefUi.java rename to quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java index 6729f7434f..b7fb2ed827 100644 --- a/src/com/android/launcher3/config/FlagTogglerPrefUi.java +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.config; +package com.android.launcher3.uioverrides.flags; import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME; @@ -33,7 +33,10 @@ import androidx.preference.PreferenceGroup; import androidx.preference.SwitchPreference; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags.DebugFlag; +import com.android.launcher3.config.FeatureFlags; + +import java.util.List; +import java.util.Set; /** * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}. @@ -50,26 +53,15 @@ public final class FlagTogglerPrefUi { @Override public void putBoolean(String key, boolean value) { - for (DebugFlag flag : FeatureFlags.getDebugFlags()) { - if (flag.key.equals(key)) { - SharedPreferences.Editor editor = mContext.getSharedPreferences( - FLAGS_PREF_NAME, Context.MODE_PRIVATE).edit(); - if (value == flag.defaultValue) { - editor.remove(key).apply(); - } else { - editor.putBoolean(key, value).apply(); - } - updateMenu(); - } - } + mSharedPreferences.edit().putBoolean(key, value).apply(); + updateMenu(); } @Override public boolean getBoolean(String key, boolean defaultValue) { - for (DebugFlag flag : FeatureFlags.getDebugFlags()) { + for (DebugFlag flag : FlagsFactory.getDebugFlags()) { if (flag.key.equals(key)) { - return mContext.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(key, flag.defaultValue); + return mSharedPreferences.getBoolean(key, flag.defaultValue); } } return defaultValue; @@ -84,11 +76,22 @@ public final class FlagTogglerPrefUi { } public void applyTo(PreferenceGroup parent) { + Set modifiedPrefs = mSharedPreferences.getAll().keySet(); + List flags = FlagsFactory.getDebugFlags(); + flags.sort((f1, f2) -> { + // Sort first by any prefs that the user has changed, then alphabetically. + int changeComparison = Boolean.compare( + modifiedPrefs.contains(f2.key), modifiedPrefs.contains(f1.key)); + return changeComparison != 0 + ? changeComparison + : f1.key.compareToIgnoreCase(f2.key); + }); + // For flag overrides we only want to store when the engineer chose to override the // flag with a different value than the default. That way, when we flip flags in // future, engineers will pick up the new value immediately. To accomplish this, we use a // custom preference data store. - for (DebugFlag flag : FeatureFlags.getDebugFlags()) { + for (DebugFlag flag : flags) { SwitchPreference switchPreference = new SwitchPreference(mContext); switchPreference.setKey(flag.key); switchPreference.setDefaultValue(flag.defaultValue); @@ -144,11 +147,11 @@ public final class FlagTogglerPrefUi { } private boolean anyChanged() { - for (DebugFlag flag : FeatureFlags.getDebugFlags()) { + for (DebugFlag flag : FlagsFactory.getDebugFlags()) { if (getFlagStateFromSharedPrefs(flag) != flag.get()) { return true; } } return false; } -} \ No newline at end of file +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java new file mode 100644 index 0000000000..2758f032e0 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java @@ -0,0 +1,170 @@ +/* + * 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.uioverrides.flags; + +import static android.app.ActivityThread.currentApplication; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.Log; + +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags.BooleanFlag; +import com.android.launcher3.config.FeatureFlags.IntFlag; +import com.android.launcher3.util.ScreenOnTracker; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class to create various flags for system build + */ +public class FlagsFactory { + + private static final String TAG = "FlagsFactory"; + + private static final FlagsFactory INSTANCE = new FlagsFactory(); + private static final boolean FLAG_AUTO_APPLY_ENABLED = false; + + public static final String FLAGS_PREF_NAME = "featureFlags"; + public static final String NAMESPACE_LAUNCHER = "launcher"; + + private static final List sDebugFlags = new ArrayList<>(); + + private final Set mKeySet = new HashSet<>(); + private boolean mRestartRequested = false; + + private FlagsFactory() { + if (!FLAG_AUTO_APPLY_ENABLED) { + return; + } +// DeviceConfig.addOnPropertiesChangedListener( +// NAMESPACE_LAUNCHER, UI_HELPER_EXECUTOR, this::onPropertiesChanged); + } + + /** + * Creates a new debug flag + */ + public static BooleanFlag getDebugFlag( + int bugId, String key, boolean defaultValue, String description) { + if (Utilities.IS_DEBUG_DEVICE) { + SharedPreferences prefs = currentApplication() + .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE); + boolean currentValue = prefs.getBoolean(key, defaultValue); + DebugFlag flag = new DebugFlag(key, description, defaultValue, currentValue); + sDebugFlags.add(flag); + return flag; + } else { + return new BooleanFlag(defaultValue); + } + } + + /** + * Creates a new release flag + */ + public static BooleanFlag getReleaseFlag( + int bugId, String key, boolean defaultValueInCode, String description) { + INSTANCE.mKeySet.add(key); +// boolean defaultValue = DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValueInCode); + if (Utilities.IS_DEBUG_DEVICE) { + SharedPreferences prefs = currentApplication() + .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE); + boolean currentValue = prefs.getBoolean(key, defaultValueInCode); +// DebugFlag flag = new DeviceFlag(key, description, defaultValue, currentValue, +// defaultValueInCode); +// sDebugFlags.add(flag); + return new BooleanFlag(defaultValueInCode); + } else { + return new BooleanFlag(defaultValueInCode); + } + } + + /** + * Creates a new integer flag. Integer flags are always release flags + */ + public static IntFlag getIntFlag( + int bugId, String key, int defaultValueInCode, String description) { + INSTANCE.mKeySet.add(key); + return new IntFlag(DeviceConfig.getInt(NAMESPACE_LAUNCHER, key, defaultValueInCode)); + } + + static List getDebugFlags() { + if (!Utilities.IS_DEBUG_DEVICE) { + return Collections.emptyList(); + } + synchronized (sDebugFlags) { + return new ArrayList<>(sDebugFlags); + } + } + + /** + * Dumps the current flags state to the print writer + */ + public static void dump(PrintWriter pw) { + if (!Utilities.IS_DEBUG_DEVICE) { + return; + } + pw.println("DeviceFlags:"); + synchronized (sDebugFlags) { + for (DebugFlag flag : sDebugFlags) { + if (flag instanceof DeviceFlag) { + pw.println(" " + flag); + } + } + } + pw.println("DebugFlags:"); + synchronized (sDebugFlags) { + for (DebugFlag flag : sDebugFlags) { + if (!(flag instanceof DeviceFlag)) { + pw.println(" " + flag); + } + } + } + } + + private void onPropertiesChanged(Properties properties) { + if (!Collections.disjoint(properties.getKeyset(), mKeySet)) { + // Schedule a restart + if (mRestartRequested) { + return; + } + Log.e(TAG, "Flag changed, scheduling restart"); + mRestartRequested = true; + ScreenOnTracker sot = ScreenOnTracker.INSTANCE.get(currentApplication()); + if (sot.isScreenOn()) { + sot.addListener(this::onScreenOnChanged); + } else { + onScreenOnChanged(false); + } + } + } + + private void onScreenOnChanged(boolean isOn) { + if (mRestartRequested && !isOn) { + Log.e(TAG, "Restart requested, killing process"); + System.exit(0); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java index 5afeca7e3b..faa900b714 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java @@ -18,11 +18,11 @@ import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; -import com.android.launcher3.Utilities; -import com.android.systemui.shared.plugins.PluginEnabler; - import androidx.preference.PreferenceDataStore; +import com.android.launcher3.LauncherPrefs; +import com.android.systemui.shared.plugins.PluginEnabler; + public class PluginEnablerImpl extends PreferenceDataStore implements PluginEnabler { private static final String PREFIX_PLUGIN_ENABLED = "PLUGIN_ENABLED_"; @@ -30,7 +30,7 @@ public class PluginEnablerImpl extends PreferenceDataStore implements PluginEnab final private SharedPreferences mSharedPrefs; public PluginEnablerImpl(Context context) { - mSharedPrefs = Utilities.getDevicePrefs(context); + mSharedPrefs = LauncherPrefs.getDevicePrefs(context); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java index 8268e594e3..d26df6acf8 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java @@ -30,17 +30,17 @@ import com.android.launcher3.Utilities; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.PluginManager; import com.android.systemui.shared.plugins.PluginActionManager; import com.android.systemui.shared.plugins.PluginInstance; -import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.plugins.PluginManagerImpl; import com.android.systemui.shared.plugins.PluginPrefs; +import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Set; public class PluginManagerWrapper { @@ -50,6 +50,9 @@ public class PluginManagerWrapper { public static final String PLUGIN_CHANGED = PluginManager.PLUGIN_CHANGED; + private static final UncaughtExceptionPreHandlerManager UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER = + new UncaughtExceptionPreHandlerManager(); + private final Context mContext; private final PluginManager mPluginManager; private final PluginEnablerImpl mPluginEnabler; @@ -69,7 +72,7 @@ public class PluginManagerWrapper { mPluginManager = new PluginManagerImpl(c, instanceManagerFactory, Utilities.IS_DEBUG_DEVICE, - Optional.ofNullable(Thread.getDefaultUncaughtExceptionHandler()), mPluginEnabler, + UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER, mPluginEnabler, new PluginPrefs(c), privilegedPlugins); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java index 899e5a4da2..c8fb2a2890 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -23,8 +23,8 @@ import android.content.Context; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; import app.lawnchair.LawnchairLauncher; import app.lawnchair.util.LawnchairUtilsKt; @@ -34,28 +34,23 @@ import app.lawnchair.util.LawnchairUtilsKt; */ public class AllAppsState extends LauncherState { - private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE | FLAG_CLOSE_POPUPS; - - private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { - @Override - public float getPageAlpha(int pageIndex) { - return 0; - } - }; + private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE | FLAG_CLOSE_POPUPS | FLAG_HOTSEAT_INACCESSIBLE; public AllAppsState(int id) { super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS); } @Override - public int getTransitionDuration(Context context) { - return 320; + public int getTransitionDuration( + DEVICE_PROFILE_CONTEXT context, boolean isToState) { + return isToState + ? context.getDeviceProfile().allAppsOpenDuration + : context.getDeviceProfile().allAppsCloseDuration; } @Override public String getDescription(Launcher launcher) { - AllAppsContainerView appsView = launcher.getAppsView(); - return appsView.getDescription(); + return launcher.getAppsView().getDescription(); } @Override @@ -65,32 +60,55 @@ public class AllAppsState extends LauncherState { @Override public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) { - ScaleAndTranslation scaleAndTranslation = LauncherState.OVERVIEW - .getWorkspaceScaleAndTranslation(launcher); - scaleAndTranslation.scale = 1; - return scaleAndTranslation; + return new ScaleAndTranslation(launcher.getDeviceProfile().workspaceContentScale, NO_OFFSET, + NO_OFFSET); } @Override - public boolean isTaskbarStashed(Launcher launcher) { - return true; + public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) { + if (launcher.getDeviceProfile().isTablet) { + return getWorkspaceScaleAndTranslation(launcher); + } else { + ScaleAndTranslation overviewScaleAndTranslation = LauncherState.OVERVIEW + .getWorkspaceScaleAndTranslation(launcher); + return new ScaleAndTranslation( + launcher.getDeviceProfile().workspaceContentScale, + overviewScaleAndTranslation.translationX, + overviewScaleAndTranslation.translationY); + } } @Override - protected float getDepthUnchecked(Context context) { - // The scrim fades in at approximately 50% of the swipe gesture. - // This means that the depth should be greater than 1, in order to fully zoom out. - return 2f; + protected float getDepthUnchecked( + DEVICE_PROFILE_CONTEXT context) { + if (context.getDeviceProfile().isTablet) { + return context.getDeviceProfile().bottomSheetDepth; + } else { + // The scrim fades in at approximately 50% of the swipe gesture. + // This means that the depth should be greater than 1, in order to fully zoom + // out. + return 2f; + } } @Override public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { - return PAGE_ALPHA_PROVIDER; + PageAlphaProvider superPageAlphaProvider = super.getWorkspacePageAlphaProvider(launcher); + return new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return launcher.getDeviceProfile().isTablet + ? superPageAlphaProvider.getPageAlpha(pageIndex) + : 0; + } + }; } @Override public int getVisibleElements(Launcher launcher) { - return ALL_APPS_CONTENT; + // Don't add HOTSEAT_ICONS for non-tablets in ALL_APPS state. + return launcher.getDeviceProfile().isTablet ? ALL_APPS_CONTENT | HOTSEAT_ICONS + : ALL_APPS_CONTENT; } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java index b7330072d4..e5787209d2 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java @@ -16,6 +16,7 @@ package com.android.launcher3.uioverrides.states; 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; @@ -23,9 +24,9 @@ import android.graphics.Color; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; -import com.android.launcher3.R; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; /** @@ -82,20 +83,45 @@ public class BackgroundAppState extends OverviewState { return false; } + @Override + public boolean showTaskThumbnailSplash() { + return true; + } + @Override protected float getDepthUnchecked(Context context) { + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + if (Launcher.getLauncher(context).areFreeformTasksVisible()) { + // Don't blur the background while freeform tasks are visible + return 0; + } + } return 1; } @Override public int getWorkspaceScrimColor(Launcher launcher) { - DeviceProfile dp = launcher.getDeviceProfile(); - if (dp.isTaskbarPresentInApps) { - return launcher.getColor(R.color.taskbar_background); - } return Color.TRANSPARENT; } + @Override + public boolean isTaskbarAlignedWithHotseat(Launcher launcher) { + if (ENABLE_SHELL_TRANSITIONS) return false; + return super.isTaskbarAlignedWithHotseat(launcher); + } + + @Override + public boolean disallowTaskbarGlobalDrag() { + // Enable global drag in overview + return false; + } + + @Override + public boolean allowTaskbarInitialSplitSelection() { + // Disallow split select from taskbar items in overview + return false; + } + public static float[] getOverviewScaleAndOffsetForBackgroundState( BaseDraggingActivity activity) { return new float[] { diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java index 6f084a1f97..b9221eef9d 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java @@ -18,12 +18,12 @@ package com.android.launcher3.uioverrides.states; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; import android.content.Context; -import android.graphics.Point; import android.graphics.Rect; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.config.FeatureFlags; import com.android.quickstep.views.RecentsView; /** @@ -40,7 +40,7 @@ public class OverviewModalTaskState extends OverviewState { } @Override - public int getTransitionDuration(Context launcher) { + public int getTransitionDuration(Context launcher, boolean isToState) { return 300; } @@ -70,13 +70,22 @@ public class OverviewModalTaskState extends OverviewState { } } - public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) { - Point taskSize = activity.getOverviewPanel().getSelectedTaskSize(); - Rect modalTaskSize = new Rect(); - activity.getOverviewPanel().getModalTaskSize(modalTaskSize); + @Override + public boolean isTaskbarStashed(Launcher launcher) { + if (FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW.get()) { + return true; + } + return super.isTaskbarStashed(launcher); + } - float scale = Math.min((float) modalTaskSize.height() / taskSize.y, - (float) modalTaskSize.width() / taskSize.x); + public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) { + RecentsView recentsView = activity.getOverviewPanel(); + Rect taskSize = recentsView.getSelectedTaskBounds(); + Rect modalTaskSize = new Rect(); + recentsView.getModalTaskSize(modalTaskSize); + + float scale = Math.min((float) modalTaskSize.height() / taskSize.height(), + (float) modalTaskSize.width() / taskSize.width()); return new float[] {scale, NO_OFFSET}; } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java index 8c0d1c46a3..6f17221d5b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java @@ -26,7 +26,8 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; -import com.android.quickstep.SysUINavigationMode; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.Themes; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -39,6 +40,10 @@ import app.lawnchair.theme.color.ColorTokens; */ public class OverviewState extends LauncherState { + private static final int OVERVIEW_SLIDE_IN_DURATION = 380; + private static final int OVERVIEW_POP_IN_DURATION = 250; + private static final int OVERVIEW_EXIT_DURATION = 250; + protected static final Rect sTempRect = new Rect(); private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED @@ -58,24 +63,41 @@ public class OverviewState extends LauncherState { } @Override - public int getTransitionDuration(Context context) { - // In gesture modes, overview comes in all the way from the side, so give it more time. - return SysUINavigationMode.INSTANCE.get(context).getMode().hasGestures ? 380 : 250; + public int getTransitionDuration(Context context, boolean isToState) { + if (isToState) { + // In gesture modes, overview comes in all the way from the side, so give it + // more time. + return DisplayController.getNavigationMode(context).hasGestures + ? OVERVIEW_SLIDE_IN_DURATION + : OVERVIEW_POP_IN_DURATION; + } else { + // When exiting Overview, exit quickly. + return OVERVIEW_EXIT_DURATION; + } } @Override public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) { RecentsView recentsView = launcher.getOverviewPanel(); - float workspacePageWidth = launcher.getDeviceProfile().getWorkspaceWidth(); recentsView.getTaskSize(sTempRect); - float scale = (float) sTempRect.width() / workspacePageWidth; + float scale; + DeviceProfile deviceProfile = launcher.getDeviceProfile(); + if (deviceProfile.isTwoPanels) { + // In two panel layout, width does not include both panels or space between + // them, so + // use height instead. We do not use height for handheld, as cell layout can be + // shorter than a task and we want the workspace to scale down to task size. + scale = (float) sTempRect.height() / deviceProfile.getCellLayoutHeight(); + } else { + scale = (float) sTempRect.width() / deviceProfile.getCellLayoutWidth(); + } float parallaxFactor = 0.5f; return new ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor); } @Override public float[] getOverviewScaleAndOffset(Launcher launcher) { - return new float[] {NO_SCALE, NO_OFFSET}; + return new float[] { NO_SCALE, NO_OFFSET }; } @Override @@ -97,8 +119,8 @@ public class OverviewState extends LauncherState { } @Override - public boolean isTaskbarStashed(Launcher launcher) { - return true; + public boolean isTaskbarAlignedWithHotseat(Launcher launcher) { + return false; } @Override @@ -108,7 +130,19 @@ public class OverviewState extends LauncherState { @Override public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) { - return deviceProfile.overviewShowAsGrid; + return deviceProfile.isTablet; + } + + @Override + public boolean disallowTaskbarGlobalDrag() { + // Disable global drag in overview + return true; + } + + @Override + public boolean allowTaskbarInitialSplitSelection() { + // Allow split select from taskbar items in overview + return true; } @Override @@ -122,15 +156,20 @@ public class OverviewState extends LauncherState { @Override protected float getDepthUnchecked(Context context) { - //TODO revert when b/178661709 is fixed + // TODO revert when b/178661709 is fixed return SystemProperties.getBoolean("ro.launcher.depth.overview", true) ? 1 : 0; } @Override public void onBackPressed(Launcher launcher) { - TaskView taskView = launcher.getOverviewPanel().getRunningTaskView(); + RecentsView recentsView = launcher.getOverviewPanel(); + TaskView taskView = recentsView.getRunningTaskView(); if (taskView != null) { - taskView.launchTaskAnimated(); + if (recentsView.isTaskViewFullyVisible(taskView)) { + taskView.launchTasks(); + } else { + recentsView.snapToPage(recentsView.indexOfChild(taskView)); + } } else { super.onBackPressed(launcher); } @@ -145,14 +184,16 @@ public class OverviewState extends LauncherState { } /** - * New Overview substate that represents the overview in modal mode (one task shown on its own) + * New Overview substate that represents the overview in modal mode (one task + * shown on its own) */ public static OverviewState newModalTaskState(int id) { return new OverviewModalTaskState(id); } /** - * New Overview substate representing state where 1 app for split screen has been selected and + * New Overview substate representing state where 1 app for split screen has + * been selected and * pinned and user is selecting the second one */ public static OverviewState newSplitSelectState(int id) { diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java index 24467e194e..10d5257e9c 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java @@ -17,15 +17,22 @@ package com.android.launcher3.uioverrides.states; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; +import android.graphics.Color; + import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; import app.lawnchair.theme.color.ColorTokens; +import com.android.launcher3.util.Themes; +import com.android.quickstep.views.DesktopTaskView; /** - * State to indicate we are about to launch a recent task. Note that this state is only used when - * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler. + * State to indicate we are about to launch a recent task. Note that this state + * is only used when + * quick switching from launcher; quick switching from an app uses + * LauncherSwipeHandler. + * * @see com.android.quickstep.GestureState.GestureEndTarget#NEW_TASK */ public class QuickSwitchState extends BackgroundAppState { @@ -44,6 +51,12 @@ public class QuickSwitchState extends BackgroundAppState { @Override public int getWorkspaceScrimColor(Launcher launcher) { + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + if (launcher.areFreeformTasksVisible()) { + // No scrim while freeform tasks are visible + return Color.TRANSPARENT; + } + } DeviceProfile dp = launcher.getDeviceProfile(); if (dp.isTaskbarPresentInApps) { return launcher.getColor(R.color.taskbar_background); diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java index 75cf5cb3a5..c7cd39c100 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java @@ -22,16 +22,21 @@ import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.HINT_STATE_TWO_BUTTON; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; -import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator; +import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; +import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION; +import static com.android.launcher3.WorkspaceStateTransitionAnimation.getWorkspaceSpringScaleAnimator; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; import static com.android.launcher3.anim.Interpolators.DEACCEL_3; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED_ACCELERATE; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; import static com.android.launcher3.anim.Interpolators.INSTANT; import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.OVERSHOOT_0_75; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; import static com.android.launcher3.anim.Interpolators.clampToProgress; import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; @@ -39,16 +44,14 @@ import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y; import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; 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.uioverrides.touchcontrollers.PortraitStatesTouchController.ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD; -import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD; -import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.ALL_APPS_SCRIM_OPAQUE_THRESHOLD; -import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.ALL_APPS_SCRIM_VISIBLE_THRESHOLD; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; import android.animation.ValueAnimator; @@ -57,11 +60,12 @@ import com.android.launcher3.CellLayout; import com.android.launcher3.Hotseat; import com.android.launcher3.LauncherState; import com.android.launcher3.Workspace; -import com.android.launcher3.anim.Interpolators; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.touch.AllAppsSwipeController; import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.quickstep.SysUINavigationMode; +import com.android.launcher3.util.DisplayController; import com.android.quickstep.util.RecentsAtomicAnimationFactory; +import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.views.RecentsView; /** @@ -91,34 +95,52 @@ public class QuickstepAtomicAnimationFactory extends public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState, StateAnimationConfig config) { RecentsView overview = mActivity.getOverviewPanel(); - if (toState == NORMAL && fromState == OVERVIEW) { + if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) { + if (fromState == OVERVIEW_SPLIT_SELECT) { + config.setInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN, + clampToProgress(EMPHASIZED_ACCELERATE, 0, 0.4f)); + config.setInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, + clampToProgress(LINEAR, 0, 0.33f)); + } + config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f)); - config.setInterpolator(ANIM_SCRIM_FADE, LINEAR); + config.setInterpolator(ANIM_SCRIM_FADE, + fromState == OVERVIEW_SPLIT_SELECT + ? clampToProgress(LINEAR, 0.33f, 1) + : LINEAR); config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL); config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL); - if (SysUINavigationMode.getMode(mActivity).hasGestures + if (DisplayController.getNavigationMode(mActivity).hasGestures && overview.getTaskViewCount() > 0) { // Overview is going offscreen, so keep it at its current scale and opacity. config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME); config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME); config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, - clampToProgress(FAST_OUT_SLOW_IN, 0, 0.75f)); + fromState == OVERVIEW_SPLIT_SELECT + ? EMPHASIZED_DECELERATE + : clampToProgress(FAST_OUT_SLOW_IN, 0, 0.75f)); config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, FINAL_FRAME); + + // Scroll RecentsView to page 0 as it goes offscreen, if necessary. + int numPagesToScroll = overview.getNextPage() - DEFAULT_PAGE; + long scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION, + numPagesToScroll * PER_PAGE_SCROLL_DURATION); + config.duration = Math.max(config.duration, scrollDuration); + + // Sync scroll so that it ends before or at the same time as the taskbar animation. + if (DisplayController.isTransientTaskbar(mActivity) + && mActivity.getDeviceProfile().isTaskbarPresent) { + config.duration = Math.min(config.duration, TASKBAR_TO_HOME_DURATION); + } + overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration)); } else { config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL); config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f)); config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7); } - // Scroll RecentsView to page 0 as it goes offscreen, if necessary. - int numPagesToScroll = overview.getNextPage() - DEFAULT_PAGE; - long scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION, - numPagesToScroll * PER_PAGE_SCROLL_DURATION); - config.duration = Math.max(config.duration, scrollDuration); - overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration)); - - Workspace workspace = mActivity.getWorkspace(); + Workspace workspace = mActivity.getWorkspace(); // Start from a higher workspace scale, but only if we're invisible so we don't jump. boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE; if (isWorkspaceVisible) { @@ -139,7 +161,7 @@ public class QuickstepAtomicAnimationFactory extends } } else if ((fromState == NORMAL || fromState == HINT_STATE || fromState == HINT_STATE_TWO_BUTTON) && toState == OVERVIEW) { - if (SysUINavigationMode.getMode(mActivity).hasGestures) { + if (DisplayController.getNavigationMode(mActivity).hasGestures) { config.setInterpolator(ANIM_WORKSPACE_SCALE, fromState == NORMAL ? ACCEL : OVERSHOOT_1_2); config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL); @@ -172,18 +194,32 @@ public class QuickstepAtomicAnimationFactory extends } else if (fromState == HINT_STATE && toState == NORMAL) { config.setInterpolator(ANIM_DEPTH, DEACCEL_3); if (mHintToNormalDuration == -1) { - ValueAnimator va = getSpringScaleAnimator(mActivity, mActivity.getWorkspace(), + ValueAnimator va = getWorkspaceSpringScaleAnimator(mActivity, + mActivity.getWorkspace(), toState.getWorkspaceScaleAndTranslation(mActivity).scale); mHintToNormalDuration = (int) va.getDuration(); } config.duration = Math.max(config.duration, mHintToNormalDuration); } else if (fromState == ALL_APPS && toState == NORMAL) { - config.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, - 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD, - 1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD)); - config.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(DEACCEL, - 1 - ALL_APPS_SCRIM_OPAQUE_THRESHOLD, - 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD)); + AllAppsSwipeController.applyAllAppsToNormalConfig(mActivity, config); + } else if (fromState == NORMAL && toState == ALL_APPS) { + AllAppsSwipeController.applyNormalToAllAppsAnimConfig(mActivity, config); + } else if (fromState == OVERVIEW && toState == OVERVIEW_SPLIT_SELECT) { + SplitAnimationTimings timings = mActivity.getDeviceProfile().isTablet + ? SplitAnimationTimings.TABLET_OVERVIEW_TO_SPLIT + : SplitAnimationTimings.PHONE_OVERVIEW_TO_SPLIT; + config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, + timings.getActionsFadeStartOffset(), + timings.getActionsFadeEndOffset())); + } else if ((fromState == NORMAL || fromState == ALL_APPS) + && toState == OVERVIEW_SPLIT_SELECT) { + // Splitting from Home is currently only available on tablets + SplitAnimationTimings timings = SplitAnimationTimings.TABLET_HOME_TO_SPLIT; + config.setInterpolator(ANIM_SCRIM_FADE, clampToProgress(LINEAR, + timings.getScrimFadeInStartOffset(), + timings.getScrimFadeInEndOffset())); + config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_0_75); + config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_0_75); } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java index e79d56b631..3ae221bbeb 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java @@ -16,7 +16,10 @@ package com.android.launcher3.uioverrides.states; +import android.content.Context; + import com.android.launcher3.Launcher; +import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.views.RecentsView; /** @@ -38,4 +41,21 @@ public class SplitScreenSelectState extends OverviewState { RecentsView recentsView = launcher.getOverviewPanel(); return recentsView.getSplitSelectTranslation(); } + + @Override + public int getTransitionDuration(Context context, boolean isToState) { + boolean isTablet = ((Launcher) context).getDeviceProfile().isTablet; + if (isToState && isTablet) { + return SplitAnimationTimings.TABLET_ENTER_DURATION; + } else if (isToState && !isTablet) { + return SplitAnimationTimings.PHONE_ENTER_DURATION; + } else { + return SplitAnimationTimings.ABORT_DURATION; + } + } + + @Override + public boolean shouldPreserveDataStateOnReapply() { + return true; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java index 86c42caa7b..40dfd82f5d 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java @@ -21,11 +21,10 @@ import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRES import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; -import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PULL_BACK_ALPHA; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PULL_BACK_TRANSLATION; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.anim.Interpolators.DEACCEL_3; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; @@ -40,16 +39,14 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.TouchController; import com.android.quickstep.TaskUtils; +import com.android.quickstep.TopTaskTracker; import com.android.quickstep.util.AnimatorControllerWithResistance; -import com.android.quickstep.util.AssistantUtilities; import com.android.quickstep.util.OverviewToHomeAnim; import com.android.quickstep.views.RecentsView; @@ -107,12 +104,13 @@ public class NavBarToHomeTouchController implements TouchController, if (mStartState.overviewUi || mStartState == ALL_APPS) { return true; } - int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL; + int typeToClose = TYPE_ALL & ~TYPE_ALL_APPS_EDU; if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) { return true; } if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get() - && AssistantUtilities.isExcludedAssistantRunning()) { + && TopTaskTracker.INSTANCE.get(mLauncher).getCachedTopTask(false) + .isExcludedAssistant()) { return true; } return false; @@ -140,23 +138,15 @@ public class NavBarToHomeTouchController implements TouchController, AnimatorControllerWithResistance.createRecentsResistanceFromOverviewAnim(mLauncher, builder); - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - builder.addOnFrameCallback(recentsView::redrawLiveTile); - } + builder.addOnFrameCallback(recentsView::redrawLiveTile); AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_TASK_MENU); } else if (mStartState == ALL_APPS) { AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); - builder.setFloat(allAppsController, ALL_APPS_PROGRESS, - -mPullbackDistance / allAppsController.getShiftRange(), PULLBACK_INTERPOLATOR); - - // Slightly fade out all apps content to further distinguish from scrolling. - StateAnimationConfig config = new StateAnimationConfig(); - config.duration = accuracy; - config.setInterpolator(StateAnimationConfig.ANIM_ALL_APPS_FADE, Interpolators - .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f)); - - allAppsController.setAlphas(mEndState, config, builder); + builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_TRANSLATION, + -mPullbackDistance, PULLBACK_INTERPOLATOR); + builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_ALPHA, + 0.5f, PULLBACK_INTERPOLATOR); } AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher); if (topView != null) { @@ -189,11 +179,9 @@ public class NavBarToHomeTouchController implements TouchController, boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS || (velocity < 0 && fling); if (success) { - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - RecentsView recentsView = mLauncher.getOverviewPanel(); - recentsView.switchToScreenshot(null, - () -> recentsView.finishRecentsAnimation(true /* toRecents */, null)); - } + RecentsView recentsView = mLauncher.getOverviewPanel(); + recentsView.switchToScreenshot(null, + () -> recentsView.finishRecentsAnimation(true /* toRecents */, null)); if (mStartState.overviewUi) { new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState)) .animateWithVelocity(velocity); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index ef6f53e8f5..b7bafd8969 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -18,13 +18,15 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.LauncherAnimUtils.newCancelListener; +import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; -import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import android.animation.ObjectAnimator; @@ -37,12 +39,15 @@ 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; +import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.OverviewToHomeAnim; -import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.views.RecentsView; /** @@ -58,6 +63,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80; private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f; + private final VibratorWrapper mVibratorWrapper; private final RecentsView mRecentsView; private final MotionPauseDetector mMotionPauseDetector; private final float mMotionPauseMinDisplacement; @@ -78,12 +84,18 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mRecentsView = l.getOverviewPanel(); mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext()); } @Override protected boolean canInterceptTouch(MotionEvent ev) { mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; - return super.canInterceptTouch(ev); + boolean isOneHandedModeActive = (SystemUiProxy.INSTANCE.get(mLauncher) + .getLastSystemUiStateFlags() & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0; + // Reset touch slop multiplier to default 1.0f if one-handed-mode is not active + mDetector.setTouchSlopMultiplier( + isOneHandedModeActive ? ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER : 1f /* default */); + return super.canInterceptTouch(ev) && !mLauncher.isInState(HINT_STATE); } @Override @@ -109,6 +121,14 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch @Override public void onDragStart(boolean start, float startDisplacement) { + if (mLauncher.isInState(ALL_APPS)) { + LauncherTaskbarUIController controller = + ((QuickstepLauncher) mLauncher).getTaskbarUIController(); + if (controller != null) { + controller.setShouldDelayLauncherStateAnim(true); + } + } + super.onDragStart(start, startDisplacement); mMotionPauseDetector.clear(); @@ -139,6 +159,12 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch @Override public void onDragEnd(float velocity) { + LauncherTaskbarUIController controller = + ((QuickstepLauncher) mLauncher).getTaskbarUIController(); + if (controller != null) { + controller.setShouldDelayLauncherStateAnim(false); + } + if (mStartedOverview) { goToOverviewOrHomeOnDragEnd(velocity); } else { @@ -163,7 +189,12 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch // Normally we compute the duration based on the velocity and distance to the given // state, but since the hint state tracks the entire screen without a clear endpoint, we // need to manually set the duration to a reasonable value. - animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher)); + animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */)); + } + if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && + ((mFromState == NORMAL && mToState == ALL_APPS) + || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) { + mVibratorWrapper.vibrateForDragBump(); } } @@ -260,14 +291,4 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch private float dpiFromPx(float pixels) { return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics().densityDpi); } - - @Override - public void onOneHandedModeStateChanged(boolean activated) { - if (activated) { - mDetector.setTouchSlopMultiplier(ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER); - } else { - // Reset touch slop multiplier to default 1.0f - mDetector.setTouchSlopMultiplier(1f /* default */); - } - } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index dadc706cf0..847114a960 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -19,7 +19,7 @@ import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS; -import static com.android.launcher3.LauncherState.QUICK_SWITCH; +import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME; import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD; import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.anim.Interpolators.ACCEL_0_75; @@ -27,6 +27,7 @@ import static com.android.launcher3.anim.Interpolators.DEACCEL_3; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP; import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent; @@ -38,16 +39,16 @@ import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_S import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS; import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW; import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT; import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; -import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; 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.TASK_SECONDARY_TRANSLATION; +import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import android.animation.Animator; @@ -55,27 +56,27 @@ import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.graphics.PointF; -import android.util.Log; import android.view.MotionEvent; import android.view.animation.Interpolator; -import com.android.launcher3.BaseQuickstepLauncher; 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.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.BothAxesSwipeDetector; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.TouchController; -import com.android.quickstep.AnimatedFloat; +import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.MotionPauseDetector; -import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.util.WorkspaceRevealAnim; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.LauncherRecentsView; import com.android.quickstep.views.RecentsView; @@ -92,7 +93,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR; private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300; - private final BaseQuickstepLauncher mLauncher; + private final QuickstepLauncher mLauncher; private final BothAxesSwipeDetector mSwipeDetector; private final float mXRange; private final float mYRange; @@ -114,7 +115,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, private AnimatorPlaybackController mXOverviewAnim; private AnimatedFloat mYOverviewAnim; - public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) { + public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) { mLauncher = launcher; mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this); mRecentsView = mLauncher.getOverviewPanel(); @@ -164,6 +165,10 @@ public class NoButtonQuickSwitchTouchController implements TouchController, if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) { return false; } + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + // TODO(b/268075592): add support for quickswitch to/from desktop + return false; + } return true; } @@ -195,7 +200,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_SCALE, FADE_OUT_INTERPOLATOR); nonOverviewBuilder.setInterpolator(ANIM_DEPTH, FADE_OUT_INTERPOLATOR); nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR); - updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder); + updateNonOverviewAnim(QUICK_SWITCH_FROM_HOME, nonOverviewBuilder); mNonOverviewAnim.dispatchOnStart(); if (mRecentsView.getTaskViewCount() == 0) { @@ -220,17 +225,18 @@ public class NoButtonQuickSwitchTouchController implements TouchController, } private void setupOverviewAnimators() { - final LauncherState fromState = QUICK_SWITCH; + final LauncherState fromState = QUICK_SWITCH_FROM_HOME; final LauncherState toState = OVERVIEW; // Set RecentView's initial properties. RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]); ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f); - Log.d(BAD_STATE, "NBQSTC setupOverviewAnimators setContentAlpha=1"); + TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, fromState.showTaskThumbnailSplash() ? 1f : 0); mRecentsView.setContentAlpha(1); mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress()); mLauncher.getActionsView().getVisibilityAlpha().setValue( (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f); + mRecentsView.setTaskIconScaledDown(true); float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher); // As we drag right, animate the following properties: @@ -242,27 +248,9 @@ public class NoButtonQuickSwitchTouchController implements TouchController, // Use QuickSwitchState instead of OverviewState to determine scrim color, // since we need to take potential taskbar into account. xAnim.setViewBackgroundColor(mLauncher.getScrimView(), - QUICK_SWITCH.getWorkspaceScrimColor(mLauncher), LINEAR); + QUICK_SWITCH_FROM_HOME.getWorkspaceScrimColor(mLauncher), LINEAR); if (mRecentsView.getTaskViewCount() == 0) { xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR); - Log.d(BAD_STATE, "NBQSTC setupOverviewAnimators from: 0 to: 1"); - xAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - Log.d(BAD_STATE, "NBQSTC setupOverviewAnimators onStart"); - } - - @Override - public void onAnimationCancel(Animator animation) { - float alpha = mRecentsView == null ? -1 : CONTENT_ALPHA.get(mRecentsView); - Log.d(BAD_STATE, "NBQSTC setupOverviewAnimators onCancel, alpha=" + alpha); - } - - @Override - public void onAnimationEnd(Animator animation) { - Log.d(BAD_STATE, "NBQSTC setupOverviewAnimators onEnd"); - } - }); } mXOverviewAnim = xAnim.createPlaybackController(); mXOverviewAnim.dispatchOnStart(); @@ -320,6 +308,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, boolean verticalFling = mSwipeDetector.isFling(velocity.y); boolean noFling = !horizontalFling && !verticalFling; if (mMotionPauseDetector.isPaused() && noFling) { + // Going to Overview. cancelAnimations(); StateAnimationConfig config = new StateAnimationConfig(); @@ -330,6 +319,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, @Override public void onAnimationEnd(Animator animation) { onAnimationToStateCompleted(OVERVIEW); + // Animate the icon after onAnimationToStateCompleted() so it doesn't clobber. + mRecentsView.animateUpTaskIconScale(); } }); overviewAnim.start(); @@ -349,24 +340,24 @@ public class NoButtonQuickSwitchTouchController implements TouchController, } else { if (velocity.y > 0) { // Flinging right and down goes to quick switch. - targetState = QUICK_SWITCH; + targetState = QUICK_SWITCH_FROM_HOME; } else { // Flinging up and right could go either home or to quick switch. // Determine the target based on the higher velocity. targetState = Math.abs(velocity.x) > Math.abs(velocity.y) - ? QUICK_SWITCH : NORMAL; + ? QUICK_SWITCH_FROM_HOME : NORMAL; } } } else if (horizontalFling) { - targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL; + targetState = velocity.x > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL; } else if (verticalFling) { - targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL; + targetState = velocity.y > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL; } else { // If user isn't flinging, just snap to the closest state. boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f; boolean passedVerticalThreshold = mYOverviewAnim.value > 1f; targetState = passedHorizontalThreshold && !passedVerticalThreshold - ? QUICK_SWITCH : NORMAL; + ? QUICK_SWITCH_FROM_HOME : NORMAL; } // Animate the various components to the target state. @@ -428,7 +419,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, nonOverviewAnim.setFloatValues(startProgress, endProgress); mNonOverviewAnim.dispatchOnStart(); } - if (targetState == QUICK_SWITCH) { + if (targetState == QUICK_SWITCH_FROM_HOME) { // Navigating to quick switch, add scroll feedback since the first time is not // considered a scroll by the RecentsView. VibratorWrapper.INSTANCE.get(mLauncher).vibrate( @@ -451,9 +442,11 @@ public class NoButtonQuickSwitchTouchController implements TouchController, .withSrcState(LAUNCHER_STATE_HOME) .withDstState(targetState.statsLogOrdinal) .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal, - targetState.ordinal > mStartState.ordinal - ? LAUNCHER_UNKNOWN_SWIPEUP - : LAUNCHER_UNKNOWN_SWIPEDOWN)); + targetState == QUICK_SWITCH_FROM_HOME + ? LAUNCHER_QUICKSWITCH_RIGHT + : targetState.ordinal > mStartState.ordinal + ? LAUNCHER_UNKNOWN_SWIPEUP + : LAUNCHER_UNKNOWN_SWIPEDOWN)); mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState)); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java index 59ade49761..8368f9ca03 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java @@ -21,10 +21,6 @@ import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; -import static com.android.launcher3.anim.Interpolators.ACCEL; -import static com.android.launcher3.anim.Interpolators.DEACCEL; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; import android.view.MotionEvent; @@ -35,6 +31,7 @@ import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.AllAppsSwipeController; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.uioverrides.states.OverviewState; import com.android.quickstep.SystemUiProxy; @@ -49,26 +46,6 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr private static final String TAG = "PortraitStatesTouchCtrl"; - /** - * The progress at which all apps content will be fully visible. - */ - public static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f; - - /** - * Minimum clamping progress for fading in all apps content - */ - public static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f; - - /** - * Minimum clamping progress for fading in all apps scrim - */ - public static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = .1f; - - /** - * Maximum clamping progress for opaque all apps scrim - */ - public static final float ALL_APPS_SCRIM_OPAQUE_THRESHOLD = .5f; - private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper; public PortraitStatesTouchController(Launcher l) { @@ -124,38 +101,15 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr return fromState; } - private StateAnimationConfig getNormalToAllAppsAnimation() { - StateAnimationConfig builder = new StateAnimationConfig(); - builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD, - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD)); - builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(ACCEL, - ALL_APPS_SCRIM_VISIBLE_THRESHOLD, - ALL_APPS_SCRIM_OPAQUE_THRESHOLD)); - return builder; - } - - private StateAnimationConfig getAllAppsToNormalAnimation() { - StateAnimationConfig builder = new StateAnimationConfig(); - builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, - 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD, - 1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD)); - builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(DEACCEL, - 1 - ALL_APPS_SCRIM_OPAQUE_THRESHOLD, - 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD)); - return builder; - } - @Override protected StateAnimationConfig getConfigForStates( LauncherState fromState, LauncherState toState) { - final StateAnimationConfig config; + final StateAnimationConfig config = new StateAnimationConfig(); + config.userControlled = true; if (fromState == NORMAL && toState == ALL_APPS) { - config = getNormalToAllAppsAnimation(); + AllAppsSwipeController.applyNormalToAllAppsAnimConfig(mLauncher, config); } else if (fromState == ALL_APPS && toState == NORMAL) { - config = getAllAppsToNormalAnimation(); - } else { - config = new StateAnimationConfig(); + AllAppsSwipeController.applyAllAppsToNormalConfig(mLauncher, config); } return config; } @@ -221,13 +175,9 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr * @return true if the event is over the hotseat */ static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) { - return (ev.getY() >= getHotseatTop(launcher)); - } - - public static int getHotseatTop(Launcher launcher) { DeviceProfile dp = launcher.getDeviceProfile(); int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; - return launcher.getDragLayer().getHeight() - hotseatHeight; + return (ev.getY() >= (launcher.getDragLayer().getHeight() - hotseatHeight)); } @Override @@ -236,12 +186,17 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr case MotionEvent.ACTION_DOWN: InteractionJankMonitorWrapper.begin( mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS); + InteractionJankMonitorWrapper.begin( + mLauncher.getRootView(), + InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: InteractionJankMonitorWrapper.cancel( InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS); + InteractionJankMonitorWrapper.cancel( + InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE); break; } return super.onControllerInterceptTouchEvent(ev); @@ -254,13 +209,20 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr if (newToState != ALL_APPS) { InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS); } + if (newToState != NORMAL) { + InteractionJankMonitorWrapper.cancel( + InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE); + } } @Override protected void onReachedFinalState(LauncherState toState) { - super.onReinitToState(toState); + super.onReachedFinalState(toState); if (toState == ALL_APPS) { InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS); + } else if (toState == NORMAL) { + InteractionJankMonitorWrapper.end( + InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE); } } @@ -268,5 +230,7 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr protected void clearState() { super.clearState(); InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS); + InteractionJankMonitorWrapper.cancel( + InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE); } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java index 59c2859d12..f941b02065 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java @@ -16,7 +16,7 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.launcher3.LauncherState.NORMAL; -import static com.android.launcher3.LauncherState.QUICK_SWITCH; +import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME; import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.DEACCEL_2; import static com.android.launcher3.anim.Interpolators.INSTANT; @@ -29,7 +29,6 @@ import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TR 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_TRANSLATE; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; @@ -37,7 +36,6 @@ import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHO import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; -import android.util.Log; import android.view.MotionEvent; import com.android.launcher3.Launcher; @@ -46,10 +44,11 @@ import com.android.launcher3.Utilities; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.SingleAxisSwipeDetector; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.SysUINavigationMode.Mode; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.NavigationMode; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -80,6 +79,10 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) { return false; } + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + // TODO(b/268075592): add support for quickswitch to/from desktop + return false; + } return true; } @@ -89,7 +92,7 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) { return NORMAL; } - return isDragTowardPositive ? QUICK_SWITCH : NORMAL; + return isDragTowardPositive ? QUICK_SWITCH_FROM_HOME : NORMAL; } @Override @@ -112,9 +115,8 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll // Set RecentView's initial properties for coming in from the side. RECENTS_SCALE_PROPERTY.set(mOverviewPanel, - QUICK_SWITCH.getOverviewScaleAndOffset(mLauncher)[0] * 0.85f); + QUICK_SWITCH_FROM_HOME.getOverviewScaleAndOffset(mLauncher)[0] * 0.85f); ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mOverviewPanel, 1f); - Log.d(BAD_STATE, "QuickSwitchTouchController initCurrentAnimation setContentAlpha=1"); mOverviewPanel.setContentAlpha(1); mCurrentAnimation = mLauncher.getStateManager() @@ -128,7 +130,7 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll private void setupInterpolators(StateAnimationConfig stateAnimationConfig) { stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2); stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2); - if (SysUINavigationMode.getMode(mLauncher) == Mode.NO_BUTTON) { + if (DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) { // Overview lives to the left of workspace, so translate down later than over stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2); stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java index c14b6c4dfc..fd99b97004 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java @@ -38,8 +38,8 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.util.TouchController; +import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.util.VibratorWrapper; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 308bca62e4..eddc50c64f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -38,11 +38,12 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.touch.SingleAxisSwipeDetector; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.TouchController; +import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.views.BaseDragLayer; -import com.android.quickstep.SysUINavigationMode; -import com.android.quickstep.util.VibratorWrapper; +import com.android.quickstep.util.VibrationConstants; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -61,7 +62,7 @@ public abstract class TaskViewTouchController Utilities.ATLEAST_R ? VibrationEffect.Composition.PRIMITIVE_TICK : -1; public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f; public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK = - VibratorWrapper.EFFECT_TEXTURE_TICK; + VibrationConstants.EFFECT_TEXTURE_TICK; protected final T mActivity; private final SingleAxisSwipeDetector mDetector; @@ -177,7 +178,7 @@ public abstract class TaskViewTouchController // - It's the focused task if in grid view // - The task is snapped mAllowGoingDown = i == mRecentsView.getCurrentPage() - && SysUINavigationMode.getMode(mActivity).hasGestures + && DisplayController.getNavigationMode(mActivity).hasGestures && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask()) && mRecentsView.isTaskInExpectedScrollPosition(i); @@ -236,7 +237,8 @@ public abstract class TaskViewTouchController PendingAnimation pa; if (goingUp) { currentInterpolator = Interpolators.LINEAR; - pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, + pa = new PendingAnimation(maxDuration); + mRecentsView.createTaskDismissAnimation(pa, mTaskBeingDragged, true /* animateTaskView */, true /* removeTask */, maxDuration, false /* dismissingForSplitSelection*/); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java index e2747dfcbb..9f2c1d479f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java @@ -24,13 +24,11 @@ import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import android.animation.ValueAnimator; import android.os.SystemClock; -import android.util.Log; import android.view.MotionEvent; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.quickstep.SystemUiProxy; diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index ee790b23fb..9f56760be4 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -15,6 +15,7 @@ */ package com.android.quickstep; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -22,20 +23,22 @@ import static android.widget.Toast.LENGTH_SHORT; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.PagedView.INVALID_PAGE; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.uioverrides.QuickstepLauncher.ENABLE_PIP_KEEP_CLEAR_ALGORITHM; 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.SystemUiController.UI_STATE_FULLSCREEN_TASK; +import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.quickstep.GestureState.GestureEndTarget.HOME; import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK; @@ -45,10 +48,12 @@ import static com.android.quickstep.GestureState.STATE_END_TARGET_SET; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED; import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; -import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -57,8 +62,10 @@ import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; +import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; @@ -66,9 +73,12 @@ import android.graphics.RectF; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; +import android.util.Log; import android.view.MotionEvent; +import android.view.RemoteAnimationTarget; import android.view.View; import android.view.View.OnApplyWindowInsetsListener; +import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.WindowInsets; @@ -79,54 +89,62 @@ import android.window.PictureInPictureSurfaceTransaction; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.android.internal.util.LatencyTracker; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.tracing.InputConsumerProto; import com.android.launcher3.tracing.SwipeHandlerProto; import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.WindowBounds; import com.android.quickstep.BaseActivityInterface.AnimationFactory; import com.android.quickstep.GestureState.GestureEndTarget; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; +import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.ActivityInitListener; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.InputConsumerProxy; import com.android.quickstep.util.InputProxyHandlerFactory; -import com.android.quickstep.util.LauncherSplitScreenListener; import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.ProtoTracer; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.StaggeredWorkspaceAnim; +import com.android.quickstep.util.SurfaceTransaction; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.SwipePipToHomeAnimator; import com.android.quickstep.util.TaskViewSimulator; -import com.android.quickstep.util.VibratorWrapper; +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.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.systemui.shared.system.LatencyTrackerCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Optional; import java.util.function.Consumer; import app.lawnchair.util.CompatibilityKt; @@ -135,13 +153,12 @@ import app.lawnchair.util.CompatibilityKt; * Handles the navigation gestures when Launcher is the default home activity. */ @TargetApi(Build.VERSION_CODES.R) -public abstract class AbsSwipeUpHandler, - Q extends RecentsView, S extends BaseState> +public abstract class AbsSwipeUpHandler, Q extends RecentsView, S extends BaseState> extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener { private static final String TAG = "AbsSwipeUpHandler"; - private static final String[] STATE_NAMES = DEBUG_STATES ? new String[17] : null; + private static final ArrayList STATE_NAMES = new ArrayList<>(); protected final BaseActivityInterface mActivityInterface; protected final InputConsumerProxy mInputConsumerProxy; @@ -150,8 +167,10 @@ public abstract class AbsSwipeUpHandler, private final ArrayList mRecentsAnimationStartCallbacks = new ArrayList<>(); private final OnScrollChangedListener mOnRecentsScrollListener = this::onRecentsViewScroll; - // Null if the recents animation hasn't started yet or has been canceled or finished. + // Null if the recents animation hasn't started yet or has been canceled or + // finished. protected @Nullable RecentsAnimationController mRecentsAnimationController; + protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController; protected RecentsAnimationTargets mRecentsAnimationTargets; protected T mActivity; protected Q mRecentsView; @@ -159,101 +178,96 @@ public abstract class AbsSwipeUpHandler, protected MultiStateCallback mStateCallback; protected boolean mCanceled; private boolean mRecentsViewScrollLinked = false; - private final ActivityLifecycleCallbacksAdapter mLifecycleCallbacks = - new ActivityLifecycleCallbacksAdapter() { - @Override - public void onActivityDestroyed(Activity activity) { - if (mActivity != activity) { - return; - } - mRecentsView = null; - mActivity = null; - } - }; - - private static int getFlagForIndex(int index, String name) { - if (DEBUG_STATES) { - STATE_NAMES[index] = name; + private final ActivityLifecycleCallbacksAdapter mLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { + @Override + public void onActivityDestroyed(Activity activity) { + if (mActivity != activity) { + return; + } + ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED); + mRecentsView = null; + mActivity = null; } - return 1 << index; + }; + + private static int FLAG_COUNT = 0; + + private static int getNextStateFlag(String name) { + if (DEBUG_STATES) { + STATE_NAMES.add(name); + } + int index = 1 << FLAG_COUNT; + FLAG_COUNT++; + return index; } // Launcher UI related states - protected static final int STATE_LAUNCHER_PRESENT = - getFlagForIndex(0, "STATE_LAUNCHER_PRESENT"); - protected static final int STATE_LAUNCHER_STARTED = - getFlagForIndex(1, "STATE_LAUNCHER_STARTED"); - protected static final int STATE_LAUNCHER_DRAWN = - getFlagForIndex(2, "STATE_LAUNCHER_DRAWN"); - // Called when the Launcher has connected to the touch interaction service (and the taskbar + protected static final int STATE_LAUNCHER_PRESENT = getNextStateFlag("STATE_LAUNCHER_PRESENT"); + protected static final int STATE_LAUNCHER_STARTED = getNextStateFlag("STATE_LAUNCHER_STARTED"); + protected static final int STATE_LAUNCHER_DRAWN = getNextStateFlag("STATE_LAUNCHER_DRAWN"); + // Called when the Launcher has connected to the touch interaction service (and + // the taskbar // ui controller is initialized) - protected static final int STATE_LAUNCHER_BIND_TO_SERVICE = - getFlagForIndex(3, "STATE_LAUNCHER_BIND_TO_SERVICE"); + protected static final int STATE_LAUNCHER_BIND_TO_SERVICE = getNextStateFlag("STATE_LAUNCHER_BIND_TO_SERVICE"); // Internal initialization states - private static final int STATE_APP_CONTROLLER_RECEIVED = - getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED"); + private static final int STATE_APP_CONTROLLER_RECEIVED = getNextStateFlag("STATE_APP_CONTROLLER_RECEIVED"); // Interaction finish states - private static final int STATE_SCALED_CONTROLLER_HOME = - getFlagForIndex(5, "STATE_SCALED_CONTROLLER_HOME"); - private static final int STATE_SCALED_CONTROLLER_RECENTS = - getFlagForIndex(6, "STATE_SCALED_CONTROLLER_RECENTS"); + private static final int STATE_SCALED_CONTROLLER_HOME = getNextStateFlag("STATE_SCALED_CONTROLLER_HOME"); + private static final int STATE_SCALED_CONTROLLER_RECENTS = getNextStateFlag("STATE_SCALED_CONTROLLER_RECENTS"); - protected static final int STATE_HANDLER_INVALIDATED = - getFlagForIndex(7, "STATE_HANDLER_INVALIDATED"); - private static final int STATE_GESTURE_STARTED = - getFlagForIndex(8, "STATE_GESTURE_STARTED"); - private static final int STATE_GESTURE_CANCELLED = - getFlagForIndex(9, "STATE_GESTURE_CANCELLED"); - private static final int STATE_GESTURE_COMPLETED = - getFlagForIndex(10, "STATE_GESTURE_COMPLETED"); + protected static final int STATE_HANDLER_INVALIDATED = getNextStateFlag("STATE_HANDLER_INVALIDATED"); + private static final int STATE_GESTURE_STARTED = getNextStateFlag("STATE_GESTURE_STARTED"); + private static final int STATE_GESTURE_CANCELLED = getNextStateFlag("STATE_GESTURE_CANCELLED"); + private static final int STATE_GESTURE_COMPLETED = getNextStateFlag("STATE_GESTURE_COMPLETED"); - private static final int STATE_CAPTURE_SCREENSHOT = - getFlagForIndex(11, "STATE_CAPTURE_SCREENSHOT"); - protected static final int STATE_SCREENSHOT_CAPTURED = - getFlagForIndex(12, "STATE_SCREENSHOT_CAPTURED"); - private static final int STATE_SCREENSHOT_VIEW_SHOWN = - getFlagForIndex(13, "STATE_SCREENSHOT_VIEW_SHOWN"); + private static final int STATE_CAPTURE_SCREENSHOT = getNextStateFlag("STATE_CAPTURE_SCREENSHOT"); + protected static final int STATE_SCREENSHOT_CAPTURED = getNextStateFlag("STATE_SCREENSHOT_CAPTURED"); + private static final int STATE_SCREENSHOT_VIEW_SHOWN = getNextStateFlag("STATE_SCREENSHOT_VIEW_SHOWN"); - private static final int STATE_RESUME_LAST_TASK = - getFlagForIndex(14, "STATE_RESUME_LAST_TASK"); - private static final int STATE_START_NEW_TASK = - getFlagForIndex(15, "STATE_START_NEW_TASK"); - private static final int STATE_CURRENT_TASK_FINISHED = - getFlagForIndex(16, "STATE_CURRENT_TASK_FINISHED"); - private static final int STATE_FINISH_WITH_NO_END = - getFlagForIndex(17, "STATE_FINISH_WITH_NO_END"); + private static final int STATE_RESUME_LAST_TASK = getNextStateFlag("STATE_RESUME_LAST_TASK"); + private static final int STATE_START_NEW_TASK = getNextStateFlag("STATE_START_NEW_TASK"); + private static final int STATE_CURRENT_TASK_FINISHED = getNextStateFlag("STATE_CURRENT_TASK_FINISHED"); + private static final int STATE_FINISH_WITH_NO_END = getNextStateFlag("STATE_FINISH_WITH_NO_END"); - private static final int LAUNCHER_UI_STATES = - STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED | - STATE_LAUNCHER_BIND_TO_SERVICE; + private static final int LAUNCHER_UI_STATES = STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED + | + STATE_LAUNCHER_BIND_TO_SERVICE; public static final long MAX_SWIPE_DURATION = 350; - public static final long HOME_DURATION = StaggeredWorkspaceAnim.DURATION_MS; public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f; - private static final float SWIPE_DURATION_MULTIPLIER = - Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); + private static final float SWIPE_DURATION_MULTIPLIER = Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, + 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured"; public static final long RECENTS_ATTACH_DURATION = 300; private static final float MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS = 0.07f; + // Controls task thumbnail splash's reveal animation after landing on a task + // from quickswitch. + // These values match WindowManager/Shell starting_window_app_reveal_* config + // values. + private static final int SPLASH_FADE_OUT_DURATION = 133; + private static final int SPLASH_APP_REVEAL_DELAY = 83; + private static final int SPLASH_APP_REVEAL_DURATION = 266; + private static final int SPLASH_ANIMATION_DURATION = 349; + /** - * Used as the page index for logging when we return to the last task at the end of the gesture. + * Used as the page index for logging when we return to the last task at the end + * of the gesture. */ private static final int LOG_NO_OP_PAGE_INDEX = -1; protected final TaskAnimationManager mTaskAnimationManager; - // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise + // Either RectFSpringAnim (if animating home) or ObjectAnimator (from + // mCurrentShift) otherwise private RunningWindowAnim[] mRunningWindowAnim; // Possible second animation running at the same time as mRunningWindowAnim private Animator mParallelRunningAnim; - // Current running divider animation - private ValueAnimator mDividerAnimator; private boolean mIsMotionPaused; private boolean mHasMotionEverBeenPaused; @@ -265,11 +279,11 @@ public abstract class AbsSwipeUpHandler, private AnimatorControllerWithResistance mLauncherTransitionController; private boolean mHasEndedLauncherTransition; - private AnimationFactory mAnimationFactory = (t) -> { }; + private AnimationFactory mAnimationFactory = (t) -> { + }; private boolean mWasLauncherAlreadyVisible; - private boolean mPassedOverviewThreshold; private boolean mGestureStarted; private boolean mLogDirectionUpOrLeft = true; private PointF mDownPos; @@ -278,44 +292,102 @@ public abstract class AbsSwipeUpHandler, private final long mTouchTimeMs; private long mLauncherFrameDrawnTime; + private final int mSplashMainWindowShiftLength; + private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch; private SwipePipToHomeAnimator mSwipePipToHomeAnimator; protected boolean mIsSwipingPipToHome; // TODO(b/195473090) no split PIP for now, remove once we have more clarity - // can try to have RectFSpringAnim evaluate multiple rects at once - private final SwipePipToHomeAnimator[] mSwipePipToHomeAnimators = - new SwipePipToHomeAnimator[2]; + // can try to have RectFSpringAnim evaluate multiple rects at once + private final SwipePipToHomeAnimator[] mSwipePipToHomeAnimators = new SwipePipToHomeAnimator[2]; - // Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold + // Interpolate RecentsView scale from start of quick switch scroll until this + // scroll threshold private final float mQuickSwitchScaleScrollThreshold; + private final int mTaskbarAppWindowThreshold; + private final int mTaskbarHomeOverviewThreshold; + private final int mTaskbarCatchUpThreshold; + private final boolean mTaskbarAlreadyOpen; + private final boolean mIsTaskbarAllAppsOpen; + private final boolean mIsTransientTaskbar; + // May be set to false when mIsTransientTaskbar is true. + private boolean mCanSlowSwipeGoHome = true; + // Indicates whether the divider is shown, only used when split screen is + // activated. + private boolean mIsDividerShown = true; + + @Nullable + private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null; + public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState, - TaskAnimationManager taskAnimationManager, GestureState gestureState, - long touchTimeMs, boolean continuingLastGesture, - InputConsumerController inputConsumer) { + TaskAnimationManager taskAnimationManager, GestureState gestureState, + long touchTimeMs, boolean continuingLastGesture, + InputConsumerController inputConsumer) { super(context, deviceState, gestureState); mActivityInterface = gestureState.getActivityInterface(); mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit); - mInputConsumerProxy = - new InputConsumerProxy(context, - () -> mRecentsView.getPagedViewOrientedState().getRecentsActivityRotation(), - inputConsumer, () -> { - endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */); - endLauncherTransitionController(); - }, new InputProxyHandlerFactory(mActivityInterface, mGestureState)); + mInputConsumerProxy = new InputConsumerProxy(context, /* rotationSupplier = */ () -> { + if (mRecentsView == null) { + return ROTATION_0; + } + return mRecentsView.getPagedViewOrientedState().getRecentsActivityRotation(); + }, inputConsumer, /* callback = */ () -> { + endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */); + endLauncherTransitionController(); + }, new InputProxyHandlerFactory(mActivityInterface, mGestureState)); mTaskAnimationManager = taskAnimationManager; mTouchTimeMs = touchTimeMs; mContinuingLastGesture = continuingLastGesture; - mQuickSwitchScaleScrollThreshold = context.getResources().getDimension( - R.dimen.quick_switch_scaling_scroll_threshold); - initAfterSubclassConstructor(); + Resources res = context.getResources(); + mQuickSwitchScaleScrollThreshold = res + .getDimension(R.dimen.quick_switch_scaling_scroll_threshold); + + mSplashMainWindowShiftLength = -res + .getDimensionPixelSize(R.dimen.starting_surface_exit_animation_window_shift_length); + + initTransitionEndpoints(mRemoteTargetHandles[0].getTaskViewSimulator() + .getOrientationState().getLauncherDeviceProfile()); initStateCallbacks(); + + mIsTransientTaskbar = mDp.isTaskbarPresent + && DisplayController.isTransientTaskbar(mActivity); + TaskbarUIController controller = mActivityInterface.getTaskbarController(); + mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed(); + mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen(); + mTaskbarAppWindowThreshold = res.getDimensionPixelSize(R.dimen.taskbar_app_window_threshold); + boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen; + mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar + ? 0 + : res.getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold); + mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold); + } + + @Nullable + private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) { + if (stateFlag == STATE_GESTURE_STARTED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_STARTED; + } else if (stateFlag == STATE_GESTURE_COMPLETED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_COMPLETED; + } else if (stateFlag == STATE_GESTURE_CANCELLED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_CANCELLED; + } else if (stateFlag == STATE_SCREENSHOT_CAPTURED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_SCREENSHOT_CAPTURED; + } else if (stateFlag == STATE_CAPTURE_SCREENSHOT) { + return ActiveGestureErrorDetector.GestureEvent.STATE_CAPTURE_SCREENSHOT; + } else if (stateFlag == STATE_HANDLER_INVALIDATED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_HANDLER_INVALIDATED; + } else if (stateFlag == STATE_LAUNCHER_DRAWN) { + return ActiveGestureErrorDetector.GestureEvent.STATE_LAUNCHER_DRAWN; + } + return null; } private void initStateCallbacks() { - mStateCallback = new MultiStateCallback(STATE_NAMES); + mStateCallback = new MultiStateCallback( + STATE_NAMES.toArray(new String[0]), AbsSwipeUpHandler::getTrackedEventForState); mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, this::onLauncherPresentAndGestureStarted); @@ -368,12 +440,6 @@ public abstract class AbsSwipeUpHandler, this::resetStateForAnimationCancel); mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END, this::resetStateForAnimationCancel); - - if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { - mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT - | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT, - (b) -> mRecentsView.setRunningTaskHidden(!b)); - } } protected boolean onActivityInit(Boolean alreadyOnHome) { @@ -392,7 +458,8 @@ public abstract class AbsSwipeUpHandler, if (mActivity != null) { if (mStateCallback.hasStates(STATE_GESTURE_COMPLETED)) { - // If the activity has restarted between setting the page scroll settling callback + // If the activity has restarted between setting the page scroll settling + // callback // and actually receiving the callback, just mark the gesture completed mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED); return true; @@ -405,7 +472,8 @@ public abstract class AbsSwipeUpHandler, } mWasLauncherAlreadyVisible = alreadyOnHome; mActivity = activity; - // Override the visibility of the activity until the gesture actually starts and we swipe + // Override the visibility of the activity until the gesture actually starts and + // we swipe // up, or until we transition home and the home animation is composed if (alreadyOnHome) { mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); @@ -423,21 +491,29 @@ public abstract class AbsSwipeUpHandler, activity.runOnceOnStart(this::onLauncherStart); } - // Set up a entire animation lifecycle callback to notify the current recents view when + // Set up a entire animation lifecycle callback to notify the current recents + // view when // the animation is canceled mGestureState.runOnceAtState(STATE_RECENTS_ANIMATION_CANCELED, () -> { - HashMap snapshots = - mGestureState.consumeRecentsAnimationCanceledSnapshot(); - if (snapshots != null) { - mRecentsView.onRecentsAnimationComplete(); + HashMap snapshots = mGestureState.consumeRecentsAnimationCanceledSnapshot(); + if (snapshots != null) { + mRecentsView.switchToScreenshot(snapshots, () -> { if (mRecentsAnimationController != null) { mRecentsAnimationController.cleanupScreenshot(); + } else if (mDeferredCleanupRecentsAnimationController != null) { + mDeferredCleanupRecentsAnimationController.cleanupScreenshot(); + mDeferredCleanupRecentsAnimationController = null; } + }); + mRecentsView.onRecentsAnimationComplete(); + if (mRecentsAnimationController != null) { + mRecentsAnimationController.cleanupScreenshot(); } - }); + } + }); setupRecentsViewUi(); - linkRecentsViewScroll(); + mRecentsView.runOnPageScrollsInitialized(this::linkRecentsViewScroll); activity.runOnBindToTouchInteractionService(this::onLauncherBindToService); mActivity.registerActivityLifecycleCallbacks(mLifecycleCallbacks); @@ -445,7 +521,8 @@ public abstract class AbsSwipeUpHandler, } /** - * Return true if the window should be translated horizontally if the recents view scrolls + * Return true if the window should be translated horizontally if the recents + * view scrolls */ protected boolean moveWindowWithRecentsScroll() { return mGestureState.getEndTarget() != HOME; @@ -465,8 +542,10 @@ public abstract class AbsSwipeUpHandler, runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() .setOrientationState(mRecentsView.getPagedViewOrientedState())); - // If we've already ended the gesture and are going home, don't prepare recents UI, - // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL. + // If we've already ended the gesture and are going home, don't prepare recents + // UI, + // as that will set the state as BACKGROUND_APP, overriding the animation to + // NORMAL. if (mGestureState.getEndTarget() != HOME) { Runnable initAnimFactory = () -> { mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState, @@ -505,8 +584,7 @@ public abstract class AbsSwipeUpHandler, mHandled = true; TraceHelper.INSTANCE.endSection(traceToken); - dragLayer.post(() -> - dragLayer.getViewTreeObserver().removeOnDrawListener(this)); + dragLayer.post(() -> dragLayer.getViewTreeObserver().removeOnDrawListener(this)); if (activity != mActivity) { return; } @@ -526,11 +604,13 @@ public abstract class AbsSwipeUpHandler, } private void onLauncherPresentAndGestureStarted() { - // Re-setup the recents UI when gesture starts, as the state could have been changed during + // Re-setup the recents UI when gesture starts, as the state could have been + // changed during // that time by a previous window transition. setupRecentsViewUi(); - // For the duration of the gesture, in cases where an activity is launched while the + // For the duration of the gesture, in cases where an activity is launched while + // the // activity is not yet resumed, finish the animation to ensure we get resumed mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback( mOnDeferredActivityLaunch); @@ -546,14 +626,10 @@ public abstract class AbsSwipeUpHandler, } private void onDeferredActivityLaunch() { - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - mActivityInterface.switchRunningTaskViewToScreenshot( - null, () -> { - mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */); - }); - } else { - mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */); - } + mActivityInterface.switchRunningTaskViewToScreenshot( + null, () -> { + mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */); + }); } private void setupRecentsViewUi() { @@ -565,26 +641,14 @@ public abstract class AbsSwipeUpHandler, } protected void notifyGestureAnimationStartToRecents() { - ActivityManager.RunningTaskInfo[] runningTasks; - if (mIsSwipeForStagedSplit) { - int[] splitTaskIds = - LauncherSplitScreenListener.INSTANCE.getNoCreate().getRunningSplitTaskIds(); - runningTasks = new ActivityManager.RunningTaskInfo[splitTaskIds.length]; - for (int i = 0; i < splitTaskIds.length; i++) { - int taskId = splitTaskIds[i]; - // Order matters here, we want first indexed RunningTaskInfo to be leftTop task - for (ActivityManager.RunningTaskInfo rti : mGestureState.getRunningTasks()) { - if (taskId == rti.taskId) { - runningTasks[i] = rti; - break; - } - - } - } + Task[] runningTasks; + if (mIsSwipeForSplit) { + int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds(); + runningTasks = mGestureState.getRunningTask().getPlaceholderTasks(splitTaskIds); } else { - runningTasks = new ActivityManager.RunningTaskInfo[]{mGestureState.getRunningTask()}; + runningTasks = mGestureState.getRunningTask().getPlaceholderTasks(); } - mRecentsView.onGestureAnimationStart(runningTasks); + mRecentsView.onGestureAnimationStart(runningTasks, mDeviceState.getRotationTouchHelper()); } private void launcherFrameDrawn() { @@ -596,19 +660,19 @@ public abstract class AbsSwipeUpHandler, Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents", TraceHelper.FLAG_IGNORE_BINDERS); - LatencyTrackerCompat.logToggleRecents( - mContext, (int) (mLauncherFrameDrawnTime - mTouchTimeMs)); + LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS, + (int) (mLauncherFrameDrawnTime - mTouchTimeMs)); TraceHelper.INSTANCE.endSection(traceToken); - // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the - // high-res thumbnail loader here once we are sure that we will end up in an overview state + // This method is only called when STATE_GESTURE_STARTED is set, so we can + // enable the + // high-res thumbnail loader here once we are sure that we will end up in an + // overview state RecentsModel.INSTANCE.get(mContext).getThumbnailCache() .getHighResLoadingState().setVisible(true); DepthController depthController = mActivityInterface.getDepthController(); - if (depthController != null) { - depthController.reapplyDepth(); - } + } public MotionPauseDetector.OnMotionPauseListener getMotionPauseListener() { @@ -616,7 +680,9 @@ public abstract class AbsSwipeUpHandler, @Override public void onMotionPauseDetected() { mHasMotionEverBeenPaused = true; - maybeUpdateRecentsAttachedState(true/* animate */, true/* moveFocusedTask */); + maybeUpdateRecentsAttachedState(true/* animate */, true/* moveRunningTask */); + Optional.ofNullable(mActivityInterface.getTaskbarController()) + .ifPresent(TaskbarUIController::startTranslationSpring); performHapticFeedback(); } @@ -632,7 +698,7 @@ public abstract class AbsSwipeUpHandler, } private void maybeUpdateRecentsAttachedState(boolean animate) { - maybeUpdateRecentsAttachedState(animate, false /* moveFocusedTask */); + maybeUpdateRecentsAttachedState(animate, false /* moveRunningTask */); } /** @@ -641,14 +707,15 @@ public abstract class AbsSwipeUpHandler, * RecentsView is shown, it will appear to be attached to the window. * * Note this method has no effect unless the navigation mode is NO_BUTTON. - * @param animate whether to animate when attaching RecentsView - * @param moveFocusedTask whether to move focused task to front when attaching + * + * @param animate whether to animate when attaching RecentsView + * @param moveRunningTask whether to move running task to front when attaching */ - private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveFocusedTask) { + private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) { if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) { return; } - RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null + RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId()) : null; final boolean recentsAttachedToAppWindow; @@ -663,15 +730,17 @@ public abstract class AbsSwipeUpHandler, } else { recentsAttachedToAppWindow = mHasMotionEverBeenPaused || mIsLikelyToStartNewTask; } - if (moveFocusedTask && !mAnimationFactory.hasRecentsEverAttachedToAppWindow() + if (moveRunningTask && !mAnimationFactory.hasRecentsEverAttachedToAppWindow() && recentsAttachedToAppWindow) { - // Only move focused task if RecentsView has never been attached before, to avoid + // Only move running task if RecentsView has never been attached before, to + // avoid // TaskView jumping to new position as we move the tasks. - mRecentsView.moveFocusedTaskToFront(); + mRecentsView.moveRunningTaskToFront(); } mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate); - // Reapply window transform throughout the attach animation, as the animation affects how + // Reapply window transform throughout the attach animation, as the animation + // affects how // much the window is bound by overscroll (vs moving freely). if (animate) { ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1); @@ -688,6 +757,16 @@ public abstract class AbsSwipeUpHandler, } } + /** + * Returns threshold that needs to be met in order for motion pause to be + * allowed. + */ + public float getThresholdToAllowMotionPause() { + return mIsTransientTaskbar + ? mTaskbarHomeOverviewThreshold + : 0; + } + public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */); } @@ -708,8 +787,10 @@ public abstract class AbsSwipeUpHandler, } /** - * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME + * We don't want to change mLauncherTransitionController if + * mGestureState.getEndTarget() == HOME * (it has its own animation) or if we explicitly ended the controller already. + * * @return Whether we can create the launcher controller or update its progress. */ private boolean canCreateNewOrUpdateExistingLauncherTransitionController() { @@ -720,7 +801,8 @@ public abstract class AbsSwipeUpHandler, public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { WindowInsets result = view.onApplyWindowInsets(windowInsets); buildAnimationController(); - // Reapply the current shift to ensure it takes new insets into account, e.g. when long + // Reapply the current shift to ensure it takes new insets into account, e.g. + // when long // pressing to stash taskbar without moving the finger. updateFinalShift(); return result; @@ -731,8 +813,10 @@ public abstract class AbsSwipeUpHandler, mLauncherTransitionController = anim; if (isFirstCreation) { mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, () -> { - // Wait until the gesture is started (touch slop was passed) to start in sync with - // mWindowTransitionController. This ensures we don't hide the taskbar background + // Wait until the gesture is started (touch slop was passed) to start in sync + // with + // mWindowTransitionController. This ensures we don't hide the taskbar + // background // when long pressing to stash it, for instance. mLauncherTransitionController.getNormalController().dispatchOnStart(); updateLauncherTransitionProgress(); @@ -750,14 +834,6 @@ public abstract class AbsSwipeUpHandler, @UiThread @Override public void updateFinalShift() { - final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; - if (passed != mPassedOverviewThreshold) { - mPassedOverviewThreshold = passed; - if (mDeviceState.isTwoButtonNavMode() && !mGestureState.isHandlingAtomicEvent()) { - performHapticFeedback(); - } - } - updateSysUiFlags(mCurrentShift.value); applyScrollAndTransform(); @@ -787,9 +863,10 @@ public abstract class AbsSwipeUpHandler, // We will handle the sysui flags based on the centermost task view. mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed - || (quickswitchThresholdPassed && centermostTaskFlags != 0)); - mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed); - // Provide a hint to WM the direction that we will be settling in case the animation + || (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); @@ -804,19 +881,28 @@ public abstract class AbsSwipeUpHandler, @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { + RecentsAnimationTargets targets) { super.onRecentsAnimationStart(controller, targets); - ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length); - mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(targets); + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED && targets.hasDesktopTasks()) { + mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets); + } else { + mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(targets); + } mRecentsAnimationController = controller; mRecentsAnimationTargets = targets; + mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck(); + mSwipePipToHomeReleaseCheck.setCanRelease(true); + mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck); - // Only initialize the device profile, if it has not been initialized before, as in some + // Only initialize the device profile, if it has not been initialized before, as + // in some // configurations targets.homeContentInsets may not be correct. if (mActivity == null) { - RemoteAnimationTargetCompat primaryTaskTarget = targets.apps[0]; - // orientation state is independent of which remote target handle we use since both - // should be pointing to the same one. Just choose index 0 for now since that works for + RemoteAnimationTarget primaryTaskTarget = targets.apps[0]; + // orientation state is independent of which remote target handle we use since + // both + // should be pointing to the same one. Just choose index 0 for now since that + // works for // both split and non-split RecentsOrientedState orientationState = mRemoteTargetHandles[0].getTaskViewSimulator() .getOrientationState(); @@ -827,7 +913,8 @@ public abstract class AbsSwipeUpHandler, dp = dp.getMultiWindowProfile(mContext, new WindowBounds(overviewStackBounds, targets.homeContentInsets)); } else { - // If we are not in multi-window mode, home insets should be same as system insets. + // If we are not in multi-window mode, home insets should be same as system + // insets. dp = dp.copy(mContext); } dp.updateInsets(targets.homeContentInsets); @@ -839,28 +926,26 @@ public abstract class AbsSwipeUpHandler, // Notify when the animation starts flushOnRecentsAnimationAndLauncherBound(); - // Start hiding the divider - setDividerShown(false, false /* immediate */); - - // Only add the callback to enable the input consumer after we actually have the controller + // Only add the callback to enable the input consumer after we actually have the + // controller mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED, - mRecentsAnimationController::enableInputConsumer); + this::startInterceptingTouchesForGesture); mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); - - mPassedOverviewThreshold = false; } @Override public void onRecentsAnimationCanceled(HashMap thumbnailDatas) { - ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation"); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "cancelRecentsAnimation", + /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); mActivityInitListener.unregister(); + // Cache the recents animation controller so we can defer its cleanup to after + // having + // properly cleaned up the screenshot without accidentally using it. + mDeferredCleanupRecentsAnimationController = mRecentsAnimationController; mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); - - if (mRecentsAnimationTargets != null) { - setDividerShown(true, false /* immediate */); - } - - // Defer clearing the controller and the targets until after we've updated the state + // Defer clearing the controller and the targets until after we've updated the + // state mRecentsAnimationController = null; mRecentsAnimationTargets = null; if (mRecentsView != null) { @@ -874,6 +959,7 @@ public abstract class AbsSwipeUpHandler, TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); if (mRecentsView != null) { + final View rv = mRecentsView; mRecentsView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { boolean mHandled = false; @@ -888,27 +974,53 @@ public abstract class AbsSwipeUpHandler, InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH, 2000 /* ms timeout */); InteractionJankMonitorWrapper.begin(mRecentsView, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME); + InteractionJankMonitorWrapper.begin(mRecentsView, + InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS); - mRecentsView.post(() -> - mRecentsView.getViewTreeObserver().removeOnDrawListener(this)); + rv.post(() -> rv.getViewTreeObserver().removeOnDrawListener(this)); } }); } notifyGestureStartedAsync(); setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */); + + if (mIsTransientTaskbar && !mTaskbarAlreadyOpen && !isLikelyToStartNewTask) { + setClampScrollOffset(true); + } mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED); mGestureStarted = true; - SystemUiProxy.INSTANCE.get(mContext).notifySwipeUpGestureStarted(); } /** - * Notifies the launcher that the swipe gesture has started. This can be called multiple times. + * Sets whether or not we should clamp the scroll offset. + * This is used to avoid x-axis movement when swiping up transient taskbar. + * + * @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp + * threshold is + * met. + */ + private void setClampScrollOffset(boolean clampScrollOffset) { + if (!mIsTransientTaskbar) { + return; + } + if (mRecentsView == null) { + mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT, + () -> mRecentsView.setClampScrollOffset(clampScrollOffset)); + return; + } + mRecentsView.setClampScrollOffset(clampScrollOffset); + } + + /** + * Notifies the launcher that the swipe gesture has started. This can be called + * multiple times. */ @UiThread private void notifyGestureStartedAsync() { final T curActivity = mActivity; if (curActivity != null) { - // Once the gesture starts, we can no longer transition home through the button, so + // Once the gesture starts, we can no longer transition home through the button, + // so // reset the force override of the activity visibility mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } @@ -925,25 +1037,34 @@ public abstract class AbsSwipeUpHandler, } /** - * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen. - * @param velocity The x and y components of the velocity when the gesture ends. - * @param downPos The x and y value of where the gesture started. + * @param endVelocityPxPerMs The velocity in the direction of the nav bar to the + * middle of the + * screen. + * @param velocityPxPerMs The x and y components of the velocity when the + * gesture ends. + * @param downPos The x and y value of where the gesture started. */ @UiThread - public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) { + public void onGestureEnded(float endVelocityPxPerMs, PointF velocityPxPerMs, PointF downPos) { float flingThreshold = mContext.getResources() .getDimension(R.dimen.quickstep_fling_threshold_speed); boolean isFling = mGestureStarted && !mIsMotionPaused - && Math.abs(endVelocity) > flingThreshold; + && Math.abs(endVelocityPxPerMs) > flingThreshold; mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED); - boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x); + boolean isVelocityVertical = Math.abs(velocityPxPerMs.y) > Math.abs(velocityPxPerMs.x); if (isVelocityVertical) { - mLogDirectionUpOrLeft = velocity.y < 0; + mLogDirectionUpOrLeft = velocityPxPerMs.y < 0; } else { - mLogDirectionUpOrLeft = velocity.x < 0; + mLogDirectionUpOrLeft = velocityPxPerMs.x < 0; } mDownPos = downPos; - handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */); + Runnable handleNormalGestureEndCallback = () -> handleNormalGestureEnd( + endVelocityPxPerMs, isFling, velocityPxPerMs, /* isCancel= */ false); + if (mRecentsView != null) { + mRecentsView.runOnPageScrollsInitialized(handleNormalGestureEndCallback); + } else { + handleNormalGestureEndCallback.run(); + } } private void endRunningWindowAnim(boolean cancel) { @@ -963,7 +1084,8 @@ public abstract class AbsSwipeUpHandler, } } if (mParallelRunningAnim != null) { - // Unlike the above animation, the parallel animation won't have anything to take up + // Unlike the above animation, the parallel animation won't have anything to + // take up // the work if it's canceled, so just end it instead. mParallelRunningAnim.end(); } @@ -984,13 +1106,16 @@ public abstract class AbsSwipeUpHandler, InteractionJankMonitorWrapper.cancel( InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME); } + if (endTarget != RECENTS) { + InteractionJankMonitorWrapper.cancel( + InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS); + } switch (endTarget) { case HOME: mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT); - // Notify swipe-to-home (recents animation) is finished - SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished(); - LauncherSplitScreenListener.INSTANCE.getNoCreate().notifySwipingToHome(); + // Notify the SysUI to use fade-in animation when entering PiP + SystemUiProxy.INSTANCE.get(mContext).setPipAnimationTypeToAlpha(); break; case RECENTS: mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT @@ -1006,16 +1131,20 @@ public abstract class AbsSwipeUpHandler, } else { mStateCallback.setState(STATE_RESUME_LAST_TASK); } - if (mRecentsAnimationTargets != null) { - setDividerShown(true, true /* immediate */); - } + // Restore the divider as it resumes the last top-tasks. + setDividerShown(true); break; } - ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + endTarget); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "onSettledOnEndTarget " + endTarget, + /* gestureEvent= */ ON_SETTLED_ON_END_TARGET); } - /** @return Whether this was the task we were waiting to appear, and thus handled it. */ - protected boolean handleTaskAppeared(RemoteAnimationTargetCompat[] appearedTaskTarget) { + /** + * @return Whether this was the task we were waiting to appear, and thus handled + * it. + */ + protected boolean handleTaskAppeared(RemoteAnimationTarget[] appearedTaskTarget) { if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { return false; } @@ -1028,78 +1157,112 @@ public abstract class AbsSwipeUpHandler, return false; } - private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling, - boolean isCancel) { + private float dpiFromPx(float pixels) { + return Utilities.dpiFromPx(pixels, mContext.getResources().getDisplayMetrics().densityDpi); + } + + private GestureEndTarget calculateEndTarget( + PointF velocityPxPerMs, float endVelocityPxPerMs, boolean isFlingY, boolean isCancel) { + ActiveGestureLog.INSTANCE.addLog( + new ActiveGestureLog.CompoundString("calculateEndTarget: velocities=(x=") + .append(Float.toString(dpiFromPx(velocityPxPerMs.x))) + .append("dp/ms, y=") + .append(Float.toString(dpiFromPx(velocityPxPerMs.y))) + .append("dp/ms), angle=") + .append(Double.toString(Math.toDegrees(Math.atan2( + -velocityPxPerMs.y, velocityPxPerMs.x))))); + if (mGestureState.isHandlingAtomicEvent()) { - // Button mode, this is only used to go to recents + // Button mode, this is only used to go to recents. return RECENTS; } - final GestureEndTarget endTarget; - final boolean goingToNewTask; - if (mRecentsView != null) { - if (!hasTargets()) { - // If there are no running tasks, then we can assume that this is a continuation of - // the last gesture, but after the recents animation has finished - goingToNewTask = true; - } else { - final int runningTaskIndex = mRecentsView.getRunningTaskIndex(); - final int taskToLaunch = mRecentsView.getNextPage(); - goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex; - } - } else { - goingToNewTask = false; - } - final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; - if (!isFling) { - if (isCancel) { - endTarget = LAST_TASK; - } else if (mDeviceState.isFullyGesturalNavMode()) { - if (mIsMotionPaused) { - endTarget = RECENTS; - } else if (goingToNewTask) { - endTarget = NEW_TASK; - } else { - endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME; - } - } else { - endTarget = reachedOverviewThreshold && mGestureStarted - ? RECENTS - : goingToNewTask - ? NEW_TASK - : LAST_TASK; - } - } else { - // If swiping at a diagonal, base end target on the faster velocity. - boolean isSwipeUp = endVelocity < 0; - boolean willGoToNewTaskOnSwipeUp = - goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity); - if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) { - endTarget = HOME; - } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp) { - // If swiping at a diagonal, base end target on the faster velocity. - endTarget = NEW_TASK; - } else if (isSwipeUp) { - endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp - ? NEW_TASK : RECENTS; - } else { - endTarget = goingToNewTask ? NEW_TASK : LAST_TASK; - } + GestureEndTarget endTarget; + if (isCancel) { + endTarget = LAST_TASK; + } else if (isFlingY) { + endTarget = calculateEndTargetForFlingY(velocityPxPerMs, endVelocityPxPerMs); + } else { + endTarget = calculateEndTargetForNonFling(velocityPxPerMs); } - if (mDeviceState.isOverviewDisabled() && (endTarget == RECENTS || endTarget == LAST_TASK)) { + if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) { return LAST_TASK; } + + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED && endTarget == NEW_TASK) { + // TODO(b/268075592): add support for quickswitch to/from desktop + return LAST_TASK; + } + return endTarget; } + private GestureEndTarget calculateEndTargetForFlingY(PointF velocity, float endVelocity) { + // If swiping at a diagonal, base end target on the faster velocity direction. + final boolean willGoToNewTask = isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity); + final boolean isSwipeUp = endVelocity < 0; + if (!isSwipeUp) { + final boolean isCenteredOnNewTask = mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex(); + return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK; + } + + return willGoToNewTask ? NEW_TASK : HOME; + } + + private GestureEndTarget calculateEndTargetForNonFling(PointF velocity) { + final boolean isScrollingToNewTask = isScrollingToNewTask(); + + // Fully gestural mode. + final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources() + .getDimension(R.dimen.quickstep_fling_threshold_speed); + if (isScrollingToNewTask && isFlingX) { + // Flinging towards new task takes precedence over mIsMotionPaused (which only + // checks y-velocity). + return NEW_TASK; + } else if (mIsMotionPaused) { + return RECENTS; + } else if (isScrollingToNewTask) { + return NEW_TASK; + } + return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK; + } + + private boolean isScrollingToNewTask() { + if (mRecentsView == null) { + return false; + } + if (!hasTargets()) { + // If there are no running tasks, then we can assume that this is a continuation + // of + // the last gesture, but after the recents animation has finished. + return true; + } + int runningTaskIndex = mRecentsView.getRunningTaskIndex(); + return runningTaskIndex >= 0 && mRecentsView.getNextPage() != runningTaskIndex; + } + + /** + * Sets whether a slow swipe can go to the HOME end target when the user lets + * go. A slow swipe + * for this purpose must meet two criteria: + * 1) y-velocity is less than quickstep_fling_threshold_speed + * AND + * 2) motion pause has not been detected (possibly because + * {@link MotionPauseDetector#setDisallowPause} has been called with + * disallowPause == true) + */ + public void setCanSlowSwipeGoHome(boolean canSlowSwipeGoHome) { + mCanSlowSwipeGoHome = canSlowSwipeGoHome; + } + @UiThread - private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity, - boolean isCancel) { + private void handleNormalGestureEnd( + float endVelocityPxPerMs, boolean isFling, PointF velocityPxPerMs, boolean isCancel) { long duration = MAX_SWIPE_DURATION; float currentShift = mCurrentShift.value; - final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity, - isFling, isCancel); + final GestureEndTarget endTarget = calculateEndTarget( + velocityPxPerMs, endVelocityPxPerMs, isFling, isCancel); // Set the state, but don't notify until the animation completes mGestureState.setEndTarget(endTarget, false /* isAtomic */); mAnimationFactory.setEndTarget(endTarget); @@ -1112,16 +1275,16 @@ public abstract class AbsSwipeUpHandler, duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); startShift = currentShift; } else { - startShift = Utilities.boundToRange(currentShift - velocity.y + startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor); if (mTransitionDragLength > 0) { - float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; + float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; - // we want the page's snap velocity to approximately match the velocity at - // which the user flings, so we scale the duration by a value near to the - // derivative of the scroll interpolator at zero, ie. 2. - long baseDuration = Math.round(Math.abs(distanceToTravel / velocity.y)); - duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + // we want the page's snap velocity to approximately match the velocity at + // which the user flings, so we scale the duration by a value near to the + // derivative of the scroll interpolator at zero, ie. 2. + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); } } Interpolator interpolator; @@ -1138,7 +1301,9 @@ public abstract class AbsSwipeUpHandler, mInputConsumerProxy.enable(); } if (endTarget == HOME) { - duration = HOME_DURATION; + duration = mActivity != null && mActivity.getDeviceProfile().isTaskbarPresent + ? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS + : StaggeredWorkspaceAnim.DURATION_MS; // Early detach the nav bar once the endTarget is determined as HOME if (mRecentsAnimationController != null) { mRecentsAnimationController.detachNavigationBarFromApp(true); @@ -1146,6 +1311,13 @@ public abstract class AbsSwipeUpHandler, } else if (endTarget == RECENTS) { if (mRecentsView != null) { int nearestPage = mRecentsView.getDestinationPage(); + if (nearestPage == INVALID_PAGE) { + // Allow the snap to invalid page to catch future error cases. + Log.e(TAG, + "RecentsView destination page is invalid", + new IllegalStateException()); + } + boolean isScrolling = false; if (mRecentsView.getNextPage() != nearestPage) { // We shouldn't really scroll to the next page when swiping up to recents. @@ -1161,21 +1333,35 @@ public abstract class AbsSwipeUpHandler, duration = Math.max(duration, mRecentsView.getScroller().getDuration()); } } + } else if (endTarget == LAST_TASK && mRecentsView != null + && mRecentsView.getNextPage() != mRecentsView.getRunningTaskIndex()) { + mRecentsView.snapToPage(mRecentsView.getRunningTaskIndex(), Math.toIntExact(duration)); } - // Let RecentsView handle the scrolling to the task, which we launch in startNewTask() + // Let RecentsView handle the scrolling to the task, which we launch in + // startNewTask() // or resumeLastTask(). - if (mRecentsView != null) { - mRecentsView.setOnPageTransitionEndCallback( - () -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED)); - } else { + Runnable onPageTransitionEnd = () -> { mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED); + setClampScrollOffset(false); + }; + if (mRecentsView != null) { + ActiveGestureLog.INSTANCE + .trackEvent(ActiveGestureErrorDetector.GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK); + mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd); + } else { + onPageTransitionEnd.run(); } - animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocity); + animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs); } private void doLogGesture(GestureEndTarget endTarget, @Nullable TaskView targetTask) { + if (mDp == null || !mDp.isGestureMode || mDownPos == null) { + // We probably never received an animation controller, skip logging. + return; + } + StatsLogManager.EventEnum event; switch (endTarget) { case HOME: @@ -1199,39 +1385,36 @@ public abstract class AbsSwipeUpHandler, logger.withItemInfo(targetTask.getItemInfo()); } - DeviceProfile dp = mDp; - if (dp == null || mDownPos == null) { - // We probably never received an animation controller, skip logging. - return; - } - int pageIndex = endTarget == LAST_TASK + int pageIndex = endTarget == LAST_TASK || mRecentsView == null ? LOG_NO_OP_PAGE_INDEX : mRecentsView.getNextPage(); - // TODO: set correct container using the pageIndex + logger.withRank(pageIndex); logger.log(event); } - /** Animates to the given progress, where 0 is the current app and 1 is overview. */ + /** + * Animates to the given progress, where 0 is the current app and 1 is overview. + */ @UiThread private void animateToProgress(float start, float end, long duration, Interpolator interpolator, - GestureEndTarget target, PointF velocityPxPerMs) { + GestureEndTarget target, PointF velocityPxPerMs) { runOnRecentsAnimationAndLauncherBound(() -> animateToProgressInternal(start, end, duration, interpolator, target, velocityPxPerMs)); } protected abstract HomeAnimationFactory createHomeAnimationFactory( ArrayList launchCookies, long duration, boolean isTargetTranslucent, - boolean appCanEnterPip, RemoteAnimationTargetCompat runningTaskTarget); + boolean appCanEnterPip, RemoteAnimationTarget runningTaskTarget); private final TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() { @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, - boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { if (task.taskId == mGestureState.getRunningTaskId() - && task.configuration.windowConfiguration.getActivityType() - != ACTIVITY_TYPE_HOME) { + && task.configuration.windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME) { // Since this is an edge case, just cancel and relaunch with default activity - // options (since we don't know if there's an associated app icon to launch from) + // options (since we don't know if there's an associated app icon to launch + // from) endRunningWindowAnim(true /* cancel */); TaskStackChangeListeners.getInstance().unregisterTaskStackListener( mActivityRestartListener); @@ -1242,12 +1425,18 @@ public abstract class AbsSwipeUpHandler, @UiThread private void animateToProgressInternal(float start, float end, long duration, - Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { + Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { maybeUpdateRecentsAttachedState(); - // If we are transitioning to launcher, then listen for the activity to be restarted while + // If we are transitioning to launcher, then listen for the activity to be + // restarted while // the transition is in progress if (mGestureState.getEndTarget().isLauncher) { + // This is also called when the launcher is resumed, in order to clear the + // pending + // widgets that have yet to be configured. + DragView.removeAllViews(mActivity); + TaskStackChangeListeners.getInstance().registerTaskStackListener( mActivityRestartListener); @@ -1267,7 +1456,7 @@ public abstract class AbsSwipeUpHandler, if (mGestureState.getEndTarget() == HOME) { getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs); - final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null + final RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId()) : null; final ArrayList cookies = runningTaskTarget != null @@ -1275,24 +1464,33 @@ public abstract class AbsSwipeUpHandler, ? runningTaskTarget.taskInfo.launchCookies : new ArrayList<>(); boolean isTranslucent = runningTaskTarget != null && runningTaskTarget.isTranslucent; + boolean hasValidLeash = runningTaskTarget != null + && runningTaskTarget.leash != null + && runningTaskTarget.leash.isValid(); boolean appCanEnterPip = !mDeviceState.isPipActive() - && runningTaskTarget != null + && hasValidLeash && runningTaskTarget.allowEnterPip && runningTaskTarget.taskInfo.pictureInPictureParams != null && runningTaskTarget.taskInfo.pictureInPictureParams.isAutoEnterEnabled(); - HomeAnimationFactory homeAnimFactory = - createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip, - runningTaskTarget); - mIsSwipingPipToHome = !mIsSwipeForStagedSplit - && homeAnimFactory.supportSwipePipToHome() && appCanEnterPip; + HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(cookies, duration, isTranslucent, + appCanEnterPip, + runningTaskTarget); + mIsSwipingPipToHome = !mIsSwipeForSplit && appCanEnterPip; final RectFSpringAnim[] windowAnim; if (mIsSwipingPipToHome) { mSwipePipToHomeAnimator = createWindowAnimationToPip( homeAnimFactory, runningTaskTarget, start); mSwipePipToHomeAnimators[0] = mSwipePipToHomeAnimator; + if (mSwipePipToHomeReleaseCheck != null) { + mSwipePipToHomeReleaseCheck.setCanRelease(false); + } windowAnim = mSwipePipToHomeAnimators; } else { mSwipePipToHomeAnimator = null; + if (mSwipePipToHomeReleaseCheck != null) { + mSwipePipToHomeReleaseCheck.setCanRelease(true); + mSwipePipToHomeReleaseCheck = null; + } windowAnim = createWindowAnimationToHome(start, homeAnimFactory); windowAnim[0].addAnimatorListener(new AnimationSuccessListener() { @@ -1315,7 +1513,8 @@ public abstract class AbsSwipeUpHandler, if (windowAnimation == null) { continue; } - windowAnimation.start(mContext, velocityPxPerMs); + DeviceProfile dp = mActivity == null ? null : mActivity.getDeviceProfile(); + windowAnimation.start(mContext, dp, velocityPxPerMs); mRunningWindowAnim[i] = RunningWindowAnim.wrap(windowAnimation); } homeAnimFactory.setSwipeVelocity(velocityPxPerMs.y); @@ -1371,28 +1570,27 @@ public abstract class AbsSwipeUpHandler, } animatorSet.setDuration(duration).setInterpolator(interpolator); animatorSet.start(); - mRunningWindowAnim = new RunningWindowAnim[]{RunningWindowAnim.wrap(animatorSet)}; + mRunningWindowAnim = new RunningWindowAnim[] { RunningWindowAnim.wrap(animatorSet) }; } } - private int calculateWindowRotation(RemoteAnimationTargetCompat runningTaskTarget, - RecentsOrientedState orientationState) { + private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget, + RecentsOrientedState orientationState) { if (runningTaskTarget.rotationChange != 0 && TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { return Math.abs(runningTaskTarget.rotationChange) == ROTATION_90 - ? ROTATION_270 : ROTATION_90; + ? ROTATION_270 + : ROTATION_90; } else { return orientationState.getDisplayRotation(); } } - /** - * TODO(b/195473090) handle multiple task simulators (if needed) for PIP - */ + @Nullable private SwipePipToHomeAnimator createWindowAnimationToPip(HomeAnimationFactory homeAnimFactory, - RemoteAnimationTargetCompat runningTaskTarget, float startProgress) { + RemoteAnimationTarget runningTaskTarget, float startProgress) { // Directly animate the app to PiP (picture-in-picture) mode - final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask(); + final ActivityManager.RunningTaskInfo taskInfo = runningTaskTarget.taskInfo; final RecentsOrientedState orientationState = mRemoteTargetHandles[0].getTaskViewSimulator() .getOrientationState(); final int windowRotation = calculateWindowRotation(runningTaskTarget, orientationState); @@ -1407,47 +1605,69 @@ public abstract class AbsSwipeUpHandler, homeToWindowPositionMap.invert(windowToHomePositionMap); windowToHomePositionMap.mapRect(startRect); + final Rect hotseatKeepClearArea = getKeepClearAreaForHotseat(); final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext) .startSwipePipToHome(taskInfo.topActivity, taskInfo.topActivityInfo, runningTaskTarget.taskInfo.pictureInPictureParams, homeRotation, - mDp.hotseatBarSizePx); + hotseatKeepClearArea); + if (destinationBounds == null) { + // No destination bounds returned from SystemUI, bail early. + return null; + } + final Rect appBounds = new Rect(); + final WindowConfiguration winConfig = taskInfo.configuration.windowConfiguration; + // Adjust the appBounds for TaskBar by using the calculated window crop Rect + // from TaskViewSimulator and fallback to the bounds in TaskInfo when it's + // originated + // from windowing modes other than full-screen. + if (winConfig.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + mRemoteTargetHandles[0].getTaskViewSimulator().getCurrentCropRect().round(appBounds); + } else { + appBounds.set(winConfig.getBounds()); + } final SwipePipToHomeAnimator.Builder builder = new SwipePipToHomeAnimator.Builder() .setContext(mContext) .setTaskId(runningTaskTarget.taskId) - .setComponentName(taskInfo.topActivity) - .setLeash(runningTaskTarget.leash.getSurfaceControl()) + .setActivityInfo(taskInfo.topActivityInfo) + .setAppIconSizePx(mDp.iconSizePx) + .setLeash(runningTaskTarget.leash) .setSourceRectHint( runningTaskTarget.taskInfo.pictureInPictureParams.getSourceRectHint()) - .setAppBounds(taskInfo.configuration.windowConfiguration.getBounds()) + .setAppBounds(appBounds) .setHomeToWindowPositionMap(homeToWindowPositionMap) .setStartBounds(startRect) .setDestinationBounds(destinationBounds) .setCornerRadius(mRecentsView.getPipCornerRadius()) + .setShadowRadius(mRecentsView.getPipShadowRadius()) .setAttachedView(mRecentsView); - // We would assume home and app window always in the same rotation While homeRotation - // is not ROTATION_0 (which implies the rotation is turned on in launcher settings). + // We would assume home and app window always in the same rotation While + // homeRotation + // is not ROTATION_0 (which implies the rotation is turned on in launcher + // settings). if (homeRotation == ROTATION_0 && (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) { builder.setFromRotation(mRemoteTargetHandles[0].getTaskViewSimulator(), windowRotation, taskInfo.displayCutoutInsets); } final SwipePipToHomeAnimator swipePipToHomeAnimator = builder.build(); - AnimatorPlaybackController activityAnimationToHome = - homeAnimFactory.createActivityAnimationToHome(); + AnimatorPlaybackController activityAnimationToHome = homeAnimFactory.createActivityAnimationToHome(); swipePipToHomeAnimator.addAnimatorListener(new AnimatorListenerAdapter() { private boolean mHasAnimationEnded; + @Override public void onAnimationStart(Animator animation) { - if (mHasAnimationEnded) return; + if (mHasAnimationEnded) + return; // Ensure Launcher ends in NORMAL state activityAnimationToHome.dispatchOnStart(); } @Override public void onAnimationEnd(Animator animation) { - if (mHasAnimationEnded) return; + if (mHasAnimationEnded) + return; mHasAnimationEnded = true; activityAnimationToHome.getAnimationPlayer().end(); if (mRecentsAnimationController == null) { @@ -1460,10 +1680,47 @@ public abstract class AbsSwipeUpHandler, mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED); } }); - setupWindowAnimation(new RectFSpringAnim[]{swipePipToHomeAnimator}); + setupWindowAnimation(new RectFSpringAnim[] { swipePipToHomeAnimator }); return swipePipToHomeAnimator; } + private Rect getKeepClearAreaForHotseat() { + Rect keepClearArea; + if (!ENABLE_PIP_KEEP_CLEAR_ALGORITHM) { + // make the height equal to hotseatBarSizePx only + keepClearArea = new Rect(0, 0, 0, mDp.hotseatBarSizePx); + return keepClearArea; + } + // the keep clear area in global screen coordinates, in pixels + if (mDp.isPhone) { + if (mDp.isSeascape()) { + // in seascape the Hotseat is on the left edge of the screen + keepClearArea = new Rect(0, 0, mDp.hotseatBarSizePx, mDp.heightPx); + } else if (mDp.isLandscape) { + // in landscape the Hotseat is on the right edge of the screen + keepClearArea = new Rect(mDp.widthPx - mDp.hotseatBarSizePx, 0, + mDp.widthPx, mDp.heightPx); + } else { + // in portrait mode the Hotseat is at the bottom of the screen + keepClearArea = new Rect(0, mDp.heightPx - mDp.hotseatBarSizePx, + mDp.widthPx, mDp.heightPx); + } + } else { + // large screens have Hotseat always at the bottom of the screen + keepClearArea = new Rect(0, mDp.heightPx - mDp.hotseatBarSizePx, + mDp.widthPx, mDp.heightPx); + } + return keepClearArea; + } + + private void startInterceptingTouchesForGesture() { + if (mRecentsAnimationController == null) { + return; + } + + mRecentsAnimationController.enableInputConsumer(); + } + private void computeRecentsScrollIfInvisible() { if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) { // Views typically don't compute scroll when invisible as an optimization, @@ -1482,15 +1739,17 @@ public abstract class AbsSwipeUpHandler, } /** - * Creates an animation that transforms the current app window into the home app. - * @param startProgress The progress of {@link #mCurrentShift} to start the window from. + * Creates an animation that transforms the current app window into the home + * app. + * + * @param startProgress The progress of {@link #mCurrentShift} to start + * the window from. * @param homeAnimationFactory The home animation factory. */ @Override protected RectFSpringAnim[] createWindowAnimationToHome(float startProgress, - HomeAnimationFactory homeAnimationFactory) { - RectFSpringAnim[] anim = - super.createWindowAnimationToHome(startProgress, homeAnimationFactory); + HomeAnimationFactory homeAnimationFactory) { + RectFSpringAnim[] anim = super.createWindowAnimationToHome(startProgress, homeAnimationFactory); setupWindowAnimation(anim); return anim; } @@ -1517,7 +1776,8 @@ public abstract class AbsSwipeUpHandler, public void onConsumerAboutToBeSwitched() { if (mActivity != null) { - // In the off chance that the gesture ends before Launcher is started, we should clear + // In the off chance that the gesture ends before Launcher is started, we should + // clear // the callback here so that it doesn't update with the wrong state mActivity.clearRunOnceOnStartCallback(); resetLauncherListeners(); @@ -1540,7 +1800,6 @@ public abstract class AbsSwipeUpHandler, private void resumeLastTask() { if (mRecentsAnimationController != null) { mRecentsAnimationController.finish(false /* toRecents */, null); - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); } doLogGesture(LAST_TASK, null); reset(); @@ -1573,14 +1832,18 @@ public abstract class AbsSwipeUpHandler, } /** - * Called when we successfully startNewTask() on the task that was previously running. Normally - * we call resumeLastTask() when returning to the previously running task, but this handles a - * specific edge case: if we switch from A to B, and back to A before B appears, we need to + * Called when we successfully startNewTask() on the task that was previously + * running. Normally + * we call resumeLastTask() when returning to the previously running task, but + * this handles a + * specific edge case: if we switch from A to B, and back to A before B appears, + * we need to * start A again to ensure it stays on top. */ @androidx.annotation.CallSuper protected void onRestartPreviouslyAppearedTask() { - // Finish the controller here, since we won't get onTaskAppeared() for a task that already + // Finish the controller here, since we won't get onTaskAppeared() for a task + // that already // appeared. if (mRecentsAnimationController != null) { mRecentsAnimationController.finish(false, null); @@ -1596,23 +1859,27 @@ public abstract class AbsSwipeUpHandler, } /** - * Cancels any running animation so that the active target can be overriden by a new swipe + * Cancels any running animation so that the active target can be overriden by a + * new swipe * handler (in case of quick switch). */ private void cancelCurrentAnimation() { + ActiveGestureLog.INSTANCE.addLog( + "AbsSwipeUpHandler.cancelCurrentAnimation", + ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION); mCanceled = true; mCurrentShift.cancelAnimation(); // Cleanup when switching handlers mInputConsumerProxy.unregisterCallback(); mActivityInitListener.unregister(); - ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener); + TaskStackChangeListeners.getInstance().unregisterTaskStackListener( + mActivityRestartListener); mTaskSnapshot = null; } private void invalidateHandler() { - if (!ENABLE_QUICKSTEP_LIVE_TILE.get() || !mActivityInterface.isInLiveTileMode() - || mGestureState.getEndTarget() != RECENTS) { + if (!mActivityInterface.isInLiveTileMode() || mGestureState.getEndTarget() != RECENTS) { mInputConsumerProxy.destroy(); mTaskAnimationManager.setLiveTileCleanUpHandler(null); } @@ -1653,14 +1920,12 @@ public abstract class AbsSwipeUpHandler, } /** - * Unlike invalidateHandlerWithLauncher, this is called even when switching consumers, e.g. on - * continued quick switch gesture, which cancels the previous handler but doesn't invalidate it. + * Unlike invalidateHandlerWithLauncher, this is called even when switching + * consumers, e.g. on + * continued quick switch gesture, which cancels the previous handler but + * doesn't invalidate it. */ private void resetLauncherListeners() { - // Reset the callback for deferred activity launches - if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { - mActivityInterface.setOnDeferredActivityLaunchCallback(null); - } mActivity.getRootView().setOnApplyWindowInsetsListener(null); mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener); @@ -1670,11 +1935,8 @@ public abstract class AbsSwipeUpHandler, boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; mActivityInterface.onTransitionCancelled(wasVisible, mGestureState.getEndTarget()); - if (mRecentsAnimationTargets != null) { - setDividerShown(true, true /* immediate */); - } - - // Leave the pending invisible flag, as it may be used by wallpaper open animation. + // Leave the pending invisible flag, as it may be used by wallpaper open + // animation. if (mActivity != null) { mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); } @@ -1686,25 +1948,36 @@ public abstract class AbsSwipeUpHandler, mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); } else { final int runningTaskId = mGestureState.getRunningTaskId(); - final boolean refreshView = !ENABLE_QUICKSTEP_LIVE_TILE.get() /* refreshView */; boolean finishTransitionPosted = false; if (mRecentsAnimationController != null) { // Update the screenshot of the task if (mTaskSnapshot == null) { UI_HELPER_EXECUTOR.execute(() -> { - if (mRecentsAnimationController == null) return; - final ThumbnailData taskSnapshot = - mRecentsAnimationController.screenshotTask(runningTaskId); + if (mRecentsAnimationController == null) + return; + final ThumbnailData taskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId); + // If split case, we should update all split tasks snapshot + if (mIsSwipeForSplit) { + int[] splitTaskIds = TopTaskTracker.INSTANCE.get( + mContext).getRunningSplitTaskIds(); + for (int i = 0; i < splitTaskIds.length; i++) { + // Skip running one because done above. + if (splitTaskIds[i] == runningTaskId) + continue; + + mRecentsAnimationController.screenshotTask(splitTaskIds[i]); + } + } MAIN_EXECUTOR.execute(() -> { mTaskSnapshot = taskSnapshot; - if (!updateThumbnail(runningTaskId, refreshView)) { + if (!updateThumbnail(runningTaskId, false /* refreshView */)) { setScreenshotCapturedState(); } }); }); return; } - finishTransitionPosted = updateThumbnail(runningTaskId, refreshView); + finishTransitionPosted = updateThumbnail(runningTaskId, false /* refreshView */); } if (!finishTransitionPosted) { setScreenshotCapturedState(); @@ -1717,8 +1990,10 @@ public abstract class AbsSwipeUpHandler, boolean finishTransitionPosted = false; final TaskView taskView; if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK) { - // Capture the screenshot before finishing the transition to home or quickswitching to - // ensure it's taken in the correct orientation, but no need to update the thumbnail. + // Capture the screenshot before finishing the transition to home or + // quickswitching to + // ensure it's taken in the correct orientation, but no need to update the + // thumbnail. taskView = null; } else { taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, refreshView); @@ -1742,42 +2017,48 @@ public abstract class AbsSwipeUpHandler, } private void finishCurrentTransitionToRecents() { - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { + if (mRecentsView != null + && mActivityInterface.getDesktopVisibilityController() != null + && mActivityInterface.getDesktopVisibilityController().areFreeformTasksVisible()) { + mRecentsView.switchToScreenshot(() -> { + mRecentsView.finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, + () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); + }); + } else { mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); if (mRecentsAnimationController != null) { mRecentsAnimationController.detachNavigationBarFromApp(true); } - } else if (!hasTargets() || mRecentsAnimationController == null) { - // If there are no targets or the animation not started, then there is nothing to finish - mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); - } else { - mRecentsAnimationController.finish(true /* toRecents */, - () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true); } private void finishCurrentTransitionToHome() { if (!hasTargets() || mRecentsAnimationController == null) { - // If there are no targets or the animation not started, then there is nothing to finish + // If there are no targets or the animation not started, then there is nothing + // to finish mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); } else { maybeFinishSwipeToHome(); finishRecentsControllerToHome( () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true); + if (mSwipePipToHomeReleaseCheck != null) { + mSwipePipToHomeReleaseCheck.setCanRelease(true); + mSwipePipToHomeReleaseCheck = null; + } doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView()); } /** - * Notifies SysUI that transition is finished if applicable and also pass leash transactions + * Notifies SysUI that transition is finished if applicable and also pass leash + * transactions * from Launcher to WM. * This should happen before {@link #finishRecentsControllerToHome(Runnable)}. */ private void maybeFinishSwipeToHome() { if (mIsSwipingPipToHome && mSwipePipToHomeAnimators[0] != null) { SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome( + mSwipePipToHomeAnimator.getTaskId(), mSwipePipToHomeAnimator.getComponentName(), mSwipePipToHomeAnimator.getDestinationBounds(), mSwipePipToHomeAnimator.getContentOverlay()); @@ -1786,14 +2067,14 @@ public abstract class AbsSwipeUpHandler, mSwipePipToHomeAnimator.getFinishTransaction(), mSwipePipToHomeAnimator.getContentOverlay()); mIsSwipingPipToHome = false; - } else if (mIsSwipeForStagedSplit) { - // Transaction to hide the task to avoid flicker for entering PiP from split-screen. - PictureInPictureSurfaceTransaction tx = - new PictureInPictureSurfaceTransaction.Builder() - .setAlpha(0f) - .build(); - int[] taskIds = - LauncherSplitScreenListener.INSTANCE.getNoCreate().getRunningSplitTaskIds(); + } else if (mIsSwipeForSplit) { + // Transaction to hide the task to avoid flicker for entering PiP from + // split-screen. + PictureInPictureSurfaceTransaction tx = new PictureInPictureSurfaceTransaction.Builder() + .setAlpha(0f) + .build(); + tx.setShouldDisableCanAffectSystemUiFlags(false); + int[] taskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds(); for (int taskId : taskIds) { mRecentsAnimationController.setFinishTaskTransaction(taskId, tx, null /* overlay */); @@ -1809,31 +2090,20 @@ public abstract class AbsSwipeUpHandler, } endLauncherTransitionController(); mRecentsView.onSwipeUpAnimationSuccess(); - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - mTaskAnimationManager.setLiveTileCleanUpHandler(() -> { - mRecentsView.cleanupRemoteTargets(); - mInputConsumerProxy.destroy(); - }); - mTaskAnimationManager.enableLiveTileRestartListener(); - } + mTaskAnimationManager.setLiveTileCleanUpHandler(() -> { + mRecentsView.cleanupRemoteTargets(); + mInputConsumerProxy.destroy(); + }); + mTaskAnimationManager.enableLiveTileRestartListener(); SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG); doLogGesture(RECENTS, mRecentsView.getCurrentPageTaskView()); reset(); } - private static boolean isNotInRecents(RemoteAnimationTargetCompat app) { + private static boolean isNotInRecents(RemoteAnimationTarget app) { return app.isNotInRecents - || app.activityType == ACTIVITY_TYPE_HOME; - } - - /** - * To be called at the end of constructor of subclasses. This calls various methods which can - * depend on proper class initialization. - */ - protected void initAfterSubclassConstructor() { - initTransitionEndpoints(mRemoteTargetHandles[0].getTaskViewSimulator() - .getOrientationState().getLauncherDeviceProfile()); + || app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME; } protected void performHapticFeedback() { @@ -1851,15 +2121,13 @@ public abstract class AbsSwipeUpHandler, protected void linkRecentsViewScroll() { SurfaceTransactionApplier.create(mRecentsView, applier -> { runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams() - .setSyncTransactionApplier(applier)); - runOnRecentsAnimationAndLauncherBound(() -> - mRecentsAnimationTargets.addReleaseCheck(applier)); + .setSyncTransactionApplier(applier)); + runOnRecentsAnimationAndLauncherBound(() -> mRecentsAnimationTargets.addReleaseCheck(applier)); }); mRecentsView.addOnScrollChangedListener(mOnRecentsScrollListener); - runOnRecentsAnimationAndLauncherBound(() -> - mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController, - mRecentsAnimationTargets)); + runOnRecentsAnimationAndLauncherBound(() -> mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController, + mRecentsAnimationTargets)); mRecentsViewScrollLinked = true; } @@ -1878,6 +2146,9 @@ public abstract class AbsSwipeUpHandler, mGestureState.updateLastStartedTaskId(taskId); boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds() .contains(taskId); + if (!hasTaskPreviouslyAppeared) { + ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED); + } nextTask.launchTask(success -> { resultCallback.accept(success); if (success) { @@ -1903,8 +2174,10 @@ public abstract class AbsSwipeUpHandler, } /** - * Runs the given {@param action} if the recents animation has already started and Launcher has - * been created and bound to the TouchInteractionService, or queues it to be run when it this + * Runs the given {@param action} if the recents animation has already started + * and Launcher has + * been created and bound to the TouchInteractionService, or queues it to be run + * when it this * next happens. */ private void runOnRecentsAnimationAndLauncherBound(Runnable action) { @@ -1927,8 +2200,11 @@ public abstract class AbsSwipeUpHandler, } /** - * TODO can we remove this now that we don't finish the controller until onTaskAppeared()? - * @return whether the recents animation has started and there are valid app targets. + * TODO can we remove this now that we don't finish the controller until + * onTaskAppeared()? + * + * @return whether the recents animation has started and there are valid app + * targets. */ protected boolean hasTargets() { return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets(); @@ -1936,9 +2212,6 @@ public abstract class AbsSwipeUpHandler, @Override public void onRecentsAnimationFinished(RecentsAnimationController controller) { - if (!controller.getFinishTargetIsLauncher()) { - setDividerShown(true, false /* immediate */); - } mRecentsAnimationController = null; mRecentsAnimationTargets = null; if (mRecentsView != null) { @@ -1947,20 +2220,60 @@ public abstract class AbsSwipeUpHandler, } @Override - public void onTasksAppeared(RemoteAnimationTargetCompat[] appearedTaskTargets) { + public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) { if (mRecentsAnimationController != null) { if (handleTaskAppeared(appearedTaskTargets)) { - mRecentsAnimationController.finish(false /* toRecents */, - null /* onFinishComplete */); - mActivityInterface.onLaunchTaskSuccess(); - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); + Optional taskTargetOptional = Arrays.stream(appearedTaskTargets) + .filter(targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId()) + .findFirst(); + if (!taskTargetOptional.isPresent()) { + finishRecentsAnimationOnTasksAppeared(); + return; + } + RemoteAnimationTarget taskTarget = taskTargetOptional.get(); + TaskView taskView = mRecentsView.getTaskViewByTaskId(taskTarget.taskId); + if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) { + finishRecentsAnimationOnTasksAppeared(); + return; + } + + ViewGroup splashView = mActivity.getDragLayer(); + + // When revealing the app with launcher splash screen, make the app visible + // and behind the splash view before the splash is animated away. + SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(splashView); + SurfaceTransaction transaction = new SurfaceTransaction(); + for (RemoteAnimationTarget target : appearedTaskTargets) { + transaction.forSurface(target.leash).setAlpha(1).setLayer(-1); + } + surfaceApplier.scheduleApply(transaction); + +// SplashScreenExitAnimationUtils.startAnimations(splashView, taskTarget.leash, +// mSplashMainWindowShiftLength, new TransactionPool(), new Rect(), +// SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION, +// /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0, +// SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION, +// new AnimatorListenerAdapter() { +// @Override +// public void onAnimationEnd(Animator animation) { +// finishRecentsAnimationOnTasksAppeared(); +// } +// }); } } } + private void finishRecentsAnimationOnTasksAppeared() { + if (mRecentsAnimationController != null) { + mRecentsAnimationController.finish(false /* toRecents */, null /* onFinishComplete */); + } + ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); + } + /** - * @return The index of the TaskView in RecentsView whose taskId matches the task that will - * resume if we finish the controller. + * @return The index of the TaskView in RecentsView whose taskId matches the + * task that will + * resume if we finish the controller. */ protected int getLastAppearedTaskIndex() { return mGestureState.getLastAppearedTaskId() != -1 @@ -1969,8 +2282,9 @@ public abstract class AbsSwipeUpHandler, } /** - * @return Whether we are continuing a gesture that already landed on a new task, - * but before that task appeared. + * @return Whether we are continuing a gesture that already landed on a new + * task, + * but before that task appeared. */ protected boolean hasStartedNewTask() { return mGestureState.getLastStartedTaskId() != -1; @@ -1990,22 +2304,28 @@ public abstract class AbsSwipeUpHandler, * Applies the transform on the recents animation */ protected void applyScrollAndTransform() { - // No need to apply any transform if there is ongoing swipe-pip-to-home animator since - // that animator handles the leash solely. - boolean notSwipingPipToHome = mRecentsAnimationTargets != null && !mIsSwipingPipToHome; + // No need to apply any transform if there is ongoing swipe-to-home animator + // swipe-to-pip handles the leash solely + // swipe-to-icon animation is handled by RectFSpringAnim anim + boolean notSwipingToHome = mRecentsAnimationTargets != null + && mGestureState.getEndTarget() != HOME; boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null; + float progress = Math.max(mCurrentShift.value, getScaleProgressDueToScroll()); + int scrollOffset = setRecentsScroll ? mRecentsView.getScrollOffset() : 0; + if (progress > 0 || scrollOffset != 0) { + // Hide the divider as the tasks start moving. + setDividerShown(false); + } for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) { - AnimatorControllerWithResistance playbackController = - remoteHandle.getPlaybackController(); + AnimatorControllerWithResistance playbackController = remoteHandle.getPlaybackController(); if (playbackController != null) { - playbackController.setProgress(Math.max(mCurrentShift.value, - getScaleProgressDueToScroll()), mDragLengthFactor); + playbackController.setProgress(progress, mDragLengthFactor); } - if (notSwipingPipToHome) { + if (notSwipingToHome) { TaskViewSimulator taskViewSimulator = remoteHandle.getTaskViewSimulator(); if (setRecentsScroll) { - taskViewSimulator.setScroll(mRecentsView.getScrollOffset()); + taskViewSimulator.setScroll(scrollOffset); } taskViewSimulator.apply(remoteHandle.getTransformParams()); } @@ -2026,8 +2346,7 @@ public abstract class AbsSwipeUpHandler, mRecentsView.getLastComputedTaskSize().height()); maxScrollOffset += mRecentsView.getPageSpacing(); - float maxScaleProgress = - MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS * mRecentsView.getMaxScaleForFullScreen(); + float maxScaleProgress = MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS * mRecentsView.getMaxScaleForFullScreen(); float scaleProgress = maxScaleProgress; if (scrollOffset < mQuickSwitchScaleScrollThreshold) { @@ -2042,21 +2361,49 @@ public abstract class AbsSwipeUpHandler, return scaleProgress; } - private void setDividerShown(boolean shown, boolean immediate) { - if (mDividerAnimator != null) { - mDividerAnimator.cancel(); + /** + * Overrides the gesture displacement to keep the app window at the bottom of + * the screen while + * the transient taskbar is being swiped in. + * + * There is also a catch up period so that the window can start moving 1:1 with + * the swipe. + */ + @Override + protected float overrideDisplacementForTransientTaskbar(float displacement) { + if (!mIsTransientTaskbar) { + return displacement; } - mDividerAnimator = TaskViewUtils.createSplitAuxiliarySurfacesAnimator( - mRecentsAnimationTargets.nonApps, shown, (dividerAnimator) -> { - dividerAnimator.start(); - if (immediate) { - dividerAnimator.end(); - } - }); + + if (mTaskbarAlreadyOpen || mIsTaskbarAllAppsOpen) { + return displacement; + } + + if (displacement < mTaskbarAppWindowThreshold) { + return 0; + } + + // "Catch up" with the displacement at mTaskbarCatchUpThreshold. + if (displacement < mTaskbarCatchUpThreshold) { + return Utilities.mapToRange(displacement, mTaskbarAppWindowThreshold, + mTaskbarCatchUpThreshold, 0, mTaskbarCatchUpThreshold, ACCEL_DEACCEL); + } + + return displacement; + } + + private void setDividerShown(boolean shown) { + if (mRecentsAnimationTargets == null || mIsDividerShown == shown) { + return; + } + mIsDividerShown = shown; + TaskViewUtils.createSplitAuxiliarySurfacesAnimator( + mRecentsAnimationTargets.nonApps, shown, null /* animatorHandler */); } /** * Used for winscope tracing, see launcher_trace.proto + * * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto * @param inputConsumerProto The parent of this proto message. */ diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 1d4ed4c487..ce41c60e89 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -20,8 +20,8 @@ import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.INSTANT; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION; +import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; -import static com.android.quickstep.SysUINavigationMode.getMode; 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; @@ -31,6 +31,7 @@ import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.Context; @@ -41,6 +42,7 @@ import android.graphics.Rect; import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; +import android.view.RemoteAnimationTarget; import android.view.View; import androidx.annotation.Nullable; @@ -50,24 +52,24 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statehandlers.DepthController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.touch.PagedOrientationHandler; -import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.views.ScrimView; -import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.util.ActivityInitListener; import com.android.quickstep.util.AnimatorControllerWithResistance; -import com.android.quickstep.util.SplitScreenBounds; -import com.android.quickstep.views.OverviewActionsView; +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.ThumbnailData; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import java.util.HashMap; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; @@ -84,6 +86,8 @@ public abstract class BaseActivityInterface callback); @@ -141,6 +156,11 @@ public abstract class BaseActivityInterface thumbnailDatas, Runnable runnable) { ACTIVITY_TYPE activity = getCreatedActivity(); @@ -213,34 +231,41 @@ public abstract class BaseActivityInterface

More specifically, used for keeping track of the state of filters applied on tasks + * in {@code RecentsView} for multi-instance management. + */ +public class RecentsFilterState { + // the minimum number of tasks per package present to allow filtering + public static final int MIN_FILTERING_TASK_COUNT = 2; + + // default filter that returns true for any input + public static final Predicate DEFAULT_FILTER = (groupTask -> true); + + // the package name to filter recent tasks by + @Nullable + private String mPackageNameToFilter = null; + + // the callback that gets executed upon filter change + @Nullable + private Runnable mOnFilterUpdatedListener = null; + + // map maintaining the count for each unique base activity package name currently in the recents + @Nullable + private Map mInstanceCountMap; + + /** + * Returns {@code true} if {@code RecentsView} filters tasks by some package name. + */ + public boolean isFiltered() { + return mPackageNameToFilter != null; + } + + /** + * Returns the package name that tasks are filtered by. + */ + @Nullable + public String getPackageNameToFilter() { + return mPackageNameToFilter; + } + + + /** + * Sets a listener on any changes to the filter. + * + * @param callback listener to be executed upon filter updates + */ + public void setOnFilterUpdatedListener(@Nullable Runnable callback) { + mOnFilterUpdatedListener = callback; + } + + /** + * Updates the filter such that tasks are filtered by a certain package name. + * + * @param packageName package name of the base activity to filter tasks by; + * if null, filter is turned off + */ + public void setFilterBy(@Nullable String packageName) { + if (Objects.equals(packageName, mPackageNameToFilter)) { + return; + } + + mPackageNameToFilter = packageName; + + if (mOnFilterUpdatedListener != null) { + mOnFilterUpdatedListener.run(); + } + } + + /** + * Updates the map of package names to their count in the most recent list of tasks. + * + * @param groupTaskList the list of tasks that map update is be based on + */ + public void updateInstanceCountMap(List groupTaskList) { + mInstanceCountMap = getInstanceCountMap(groupTaskList); + } + + /** + * Returns the map of package names to their count in the most recent list of tasks. + */ + @Nullable + public Map getInstanceCountMap() { + return mInstanceCountMap; + } + + /** + * Returns a predicate for filtering out GroupTasks by package name. + * + * @param packageName package name to filter GroupTasks by + * if null, Predicate always returns true. + */ + public static Predicate getFilter(@Nullable String packageName) { + if (packageName == null) { + return DEFAULT_FILTER; + } + + return (groupTask) -> (groupTask.task2 != null + && groupTask.task2.key.getPackageName().equals(packageName)) + || groupTask.task1.key.getPackageName().equals(packageName); + } + + /** + * Returns a map of package names to their frequencies in a list of GroupTasks. + * + * @param groupTasks the list to go through to create the map + */ + public static Map getInstanceCountMap(List groupTasks) { + Map instanceCountMap = new HashMap<>(); + + for (GroupTask groupTask : groupTasks) { + final String firstTaskPkgName = groupTask.task1.key.getPackageName(); + final String secondTaskPkgName = + groupTask.task2 == null ? null : groupTask.task2.key.getPackageName(); + + // increment the instance count for the first task's base activity package name + incrementOrAddIfNotExists(instanceCountMap, firstTaskPkgName); + + // check if second task is non existent + if (secondTaskPkgName != null) { + // increment the instance count for the second task's base activity package name + incrementOrAddIfNotExists(instanceCountMap, secondTaskPkgName); + } + } + + return instanceCountMap; + } + + /** + * Returns true if tasks of provided package name should show filter UI. + * + * @param taskPackageName package name of the task in question + */ + public boolean shouldShowFilterUI(String taskPackageName) { + // number of occurrences in recents overview with the package name of this task + int instanceCount = getInstanceCountMap().get(taskPackageName); + + // if the number of occurrences isn't enough make sure tasks can't be filtered by + // the package name of this task + return !(isFiltered() || instanceCount < MIN_FILTERING_TASK_COUNT); + } + + private static void incrementOrAddIfNotExists(Map map, String pkgName) { + if (!map.containsKey(pkgName)) { + map.put(pkgName, 0); + } + map.put(pkgName, map.get(pkgName) + 1); + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index f75a6986f2..b9064563a0 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -22,6 +22,7 @@ import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; import android.annotation.TargetApi; import android.app.ActivityManager; +import android.app.KeyguardManager; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; @@ -36,10 +37,10 @@ import com.android.launcher3.icons.IconProvider.IconChangeListener; import com.android.launcher3.util.Executors.SimpleThreadFactory; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.TaskVisualsChangeListener; 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.KeyguardManagerCompat; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; @@ -49,6 +50,7 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.function.Predicate; import app.lawnchair.LawnchairApp; import app.lawnchair.icons.LawnchairIconProvider; @@ -57,7 +59,8 @@ import app.lawnchair.icons.LawnchairIconProvider; * Singleton class to load and manage recents model. */ @TargetApi(Build.VERSION_CODES.O) -public class RecentsModel extends TaskStackChangeListener implements IconChangeListener { +public class RecentsModel implements IconChangeListener, TaskStackChangeListener, + TaskVisualsChangeListener { // We do not need any synchronization for this variable as its only written on UI thread. public static final MainThreadInitializedObject INSTANCE = @@ -76,10 +79,12 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL private RecentsModel(Context context) { mContext = context; mTaskList = new RecentTasksList(MAIN_EXECUTOR, - new KeyguardManagerCompat(context), SystemUiProxy.INSTANCE.get(context)); + context.getSystemService(KeyguardManager.class), + SystemUiProxy.INSTANCE.get(context)); IconProvider iconProvider = new LawnchairIconProvider(context); mIconCache = new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider); + mIconCache.registerTaskVisualsChangeListener(this); mThumbnailCache = new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR); if (LawnchairApp.isRecentsEnabled()) { @@ -97,14 +102,30 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL } /** - * Fetches the list of recent tasks. + * Fetches the list of recent tasks. Tasks are ordered by recency, with the latest active tasks + * at the end of the list. * * @param callback The callback to receive the task plan once its complete or null. This is * always called on the UI thread. * @return the request id associated with this call. */ public int getTasks(Consumer> callback) { - return mTaskList.getTasks(false /* loadKeysOnly */, callback); + return mTaskList.getTasks(false /* loadKeysOnly */, callback, + RecentsFilterState.DEFAULT_FILTER); + } + + + /** + * Fetches the list of recent tasks, based on a filter + * + * @param callback The callback to receive the task plan once its complete or null. This is + * always called on the UI thread. + * @param filter Returns true if a GroupTask should be included into the list passed into + * callback. + * @return the request id associated with this call. + */ + public int getTasks(Consumer> callback, Predicate filter) { + return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter); } /** @@ -126,8 +147,9 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL * Checks if a task has been removed or not. * * @param callback Receives true if task is removed, false otherwise + * @param filter Returns true if GroupTask should be in the list of considerations */ - public void isTaskRemoved(int taskId, Consumer callback) { + public void isTaskRemoved(int taskId, Consumer callback, Predicate filter) { mTaskList.getTasks(true /* loadKeysOnly */, (taskGroups) -> { for (GroupTask group : taskGroups) { if (group.containsTask(taskId)) { @@ -136,7 +158,7 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL } } callback.accept(true); - }); + }, filter); } @Override @@ -170,7 +192,7 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL } @Override - public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { + public boolean onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { mThumbnailCache.updateTaskSnapShot(taskId, snapshot); for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) { @@ -179,6 +201,7 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL task.thumbnail = snapshot; } } + return true; } @Override @@ -207,6 +230,13 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL } } + @Override + public void onTaskIconChanged(int taskId) { + for (TaskVisualsChangeListener listener : mThumbnailChangeListeners) { + listener.onTaskIconChanged(taskId); + } + } + @Override public void onSystemIconStateChanged(String iconState) { mIconCache.clearCache(); @@ -232,18 +262,33 @@ public class RecentsModel extends TaskStackChangeListener implements IconChangeL } /** - * Listener for receiving various task properties changes + * Registers a listener for running tasks */ - public interface TaskVisualsChangeListener { + public void registerRunningTasksListener(RunningTasksListener listener) { + mTaskList.registerRunningTasksListener(listener); + } - /** - * Called whn the task thumbnail changes - */ - Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData); + /** + * Removes the previously registered running tasks listener + */ + public void unregisterRunningTasksListener() { + mTaskList.unregisterRunningTasksListener(); + } + /** + * Gets the set of running tasks. + */ + public ArrayList getRunningTasks() { + return mTaskList.getRunningTasks(); + } + + /** + * Listener for receiving running tasks changes + */ + public interface RunningTasksListener { /** - * Called when the icon for a task changes + * Called when there's a change to running tasks */ - void onTaskIconChanged(String pkg, UserHandle user); + void onRunningTasksChanged(); } } diff --git a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java index b20d48806a..80aaad0dd9 100644 --- a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java +++ b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java @@ -15,11 +15,11 @@ */ package com.android.quickstep; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; - -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import android.view.RemoteAnimationTarget; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; @@ -31,41 +31,40 @@ public class RemoteAnimationTargets { private final CopyOnWriteArrayList mReleaseChecks = new CopyOnWriteArrayList<>(); - public final RemoteAnimationTargetCompat[] unfilteredApps; - public final RemoteAnimationTargetCompat[] apps; - public final RemoteAnimationTargetCompat[] wallpapers; - public final RemoteAnimationTargetCompat[] nonApps; + public final RemoteAnimationTarget[] unfilteredApps; + public final RemoteAnimationTarget[] apps; + public final RemoteAnimationTarget[] wallpapers; + public final RemoteAnimationTarget[] nonApps; public final int targetMode; public final boolean hasRecents; private boolean mReleased = false; - public RemoteAnimationTargets(RemoteAnimationTargetCompat[] apps, - RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps, + public RemoteAnimationTargets(RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, int targetMode) { - ArrayList filteredApps = new ArrayList<>(); + ArrayList filteredApps = new ArrayList<>(); boolean hasRecents = false; if (apps != null) { - for (RemoteAnimationTargetCompat target : apps) { + for (RemoteAnimationTarget target : apps) { if (target.mode == targetMode) { filteredApps.add(target); } - hasRecents |= target.activityType == - RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS; + hasRecents |= target.windowConfiguration.getActivityType() == ACTIVITY_TYPE_RECENTS; } } this.unfilteredApps = apps; - this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]); + this.apps = filteredApps.toArray(new RemoteAnimationTarget[filteredApps.size()]); this.wallpapers = wallpapers; this.targetMode = targetMode; this.hasRecents = hasRecents; this.nonApps = nonApps; } - public RemoteAnimationTargetCompat findTask(int taskId) { - for (RemoteAnimationTargetCompat target : apps) { + public RemoteAnimationTarget findTask(int taskId) { + for (RemoteAnimationTarget target : apps) { if (target.taskId == taskId) { return target; } @@ -76,12 +75,12 @@ public class RemoteAnimationTargets { /** * Gets the navigation bar remote animation target if exists. */ - public RemoteAnimationTargetCompat getNavBarRemoteAnimationTarget() { + public RemoteAnimationTarget getNavBarRemoteAnimationTarget() { return getNonAppTargetOfType(TYPE_NAVIGATION_BAR); } - public RemoteAnimationTargetCompat getNonAppTargetOfType(int type) { - for (RemoteAnimationTargetCompat target : nonApps) { + public RemoteAnimationTarget getNonAppTargetOfType(int type) { + for (RemoteAnimationTarget target : nonApps) { if (target.windowType == type) { return target; } @@ -90,19 +89,19 @@ public class RemoteAnimationTargets { } /** Returns the first opening app target. */ - public RemoteAnimationTargetCompat getFirstAppTarget() { + public RemoteAnimationTarget getFirstAppTarget() { return apps.length > 0 ? apps[0] : null; } /** Returns the task id of the first opening app target, or -1 if none is found. */ public int getFirstAppTargetTaskId() { - RemoteAnimationTargetCompat target = getFirstAppTarget(); + RemoteAnimationTarget target = getFirstAppTarget(); return target == null ? -1 : target.taskId; } public boolean isAnimatingHome() { - for (RemoteAnimationTargetCompat target : unfilteredApps) { - if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + for (RemoteAnimationTarget target : unfilteredApps) { + if (target.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME) { return true; } } @@ -114,10 +113,6 @@ public class RemoteAnimationTargets { } public void release() { - if (ENABLE_SHELL_TRANSITIONS) { - mReleaseChecks.clear(); - return; - } if (mReleased) { return; } @@ -129,15 +124,19 @@ public class RemoteAnimationTargets { } mReleaseChecks.clear(); mReleased = true; + release(unfilteredApps); + release(wallpapers); + release(nonApps); + } - for (RemoteAnimationTargetCompat target : unfilteredApps) { - target.release(); - } - for (RemoteAnimationTargetCompat target : wallpapers) { - target.release(); - } - for (RemoteAnimationTargetCompat target : nonApps) { - target.release(); + private static void release(RemoteAnimationTarget[] targets) { + for (RemoteAnimationTarget target : targets) { + if (target.leash != null) { + target.leash.release(); + } + if (target.startLeash != null) { + target.startLeash.release(); + } } } diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java index ed1a06d05d..f30d3f14cc 100644 --- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java +++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java @@ -17,15 +17,18 @@ package com.android.quickstep; import android.content.Context; +import android.graphics.Rect; +import android.view.RemoteAnimationTarget; import androidx.annotation.Nullable; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.quickstep.util.AnimatorControllerWithResistance; -import com.android.quickstep.util.LauncherSplitScreenListener; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.quickstep.views.DesktopTaskView; + +import java.util.ArrayList; /** * Glues together the necessary components to animate a remote target using a @@ -33,14 +36,14 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat; */ public class RemoteTargetGluer { private RemoteTargetHandle[] mRemoteTargetHandles; - private StagedSplitBounds mStagedSplitBounds; + private SplitBounds mSplitBounds; /** * Use this constructor if remote targets are split-screen independent */ public RemoteTargetGluer(Context context, BaseActivityInterface sizingStrategy, - RemoteAnimationTargets targets) { - mRemoteTargetHandles = createHandles(context, sizingStrategy, targets.apps.length); + RemoteAnimationTargets targets, boolean forDesktop) { + init(context, sizingStrategy, targets.apps.length, forDesktop); } /** @@ -48,16 +51,31 @@ public class RemoteTargetGluer { * running tasks */ public RemoteTargetGluer(Context context, BaseActivityInterface sizingStrategy) { - int[] splitIds = LauncherSplitScreenListener.INSTANCE.getNoCreate() - .getRunningSplitTaskIds(); - mRemoteTargetHandles = createHandles(context, sizingStrategy, splitIds.length == 2 ? 2 : 1); + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + // TODO: binder call, only for prototyping. Creating the gluer should be postponed so + // we can create it when we have the remote animation targets ready. + int desktopTasks = SystemUiProxy.INSTANCE.get(context).getVisibleDesktopTaskCount(); + if (desktopTasks > 0) { + init(context, sizingStrategy, desktopTasks, true /* forDesktop */); + return; + } + } + + int[] splitIds = TopTaskTracker.INSTANCE.get(context).getRunningSplitTaskIds(); + init(context, sizingStrategy, splitIds.length == 2 ? 2 : 1, false /* forDesktop */); + } + + private void init(Context context, BaseActivityInterface sizingStrategy, int numHandles, + boolean forDesktop) { + mRemoteTargetHandles = createHandles(context, sizingStrategy, numHandles, forDesktop); } private RemoteTargetHandle[] createHandles(Context context, - BaseActivityInterface sizingStrategy, int numHandles) { + BaseActivityInterface sizingStrategy, int numHandles, boolean forDesktop) { RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles]; for (int i = 0; i < numHandles; i++) { TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy); + tvs.setIsDesktopTask(forDesktop); TransformParams transformParams = new TransformParams(); handles[i] = new RemoteTargetHandle(tvs, transformParams); } @@ -75,7 +93,7 @@ public class RemoteTargetGluer { */ public RemoteTargetHandle[] assignTargets(RemoteAnimationTargets targets) { for (int i = 0; i < mRemoteTargetHandles.length; i++) { - RemoteAnimationTargetCompat primaryTaskTarget = targets.apps[i]; + RemoteAnimationTarget primaryTaskTarget = targets.apps[i]; mRemoteTargetHandles[i].mTransformParams.setTargetSet( createRemoteAnimationTargetsForTarget(targets, null)); mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null); @@ -84,56 +102,76 @@ public class RemoteTargetGluer { } /** - * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this matches the - * apps in targets.apps to that of the _active_ split screened tasks. - * See {@link #assignTargetsForSplitScreen(RemoteAnimationTargets, int[])} + * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this assigns the + * apps in {@code targets.apps} to the {@link #mRemoteTargetHandles} with index 0 will being + * the left/top task, index 1 right/bottom. */ public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets) { - int[] splitIds = LauncherSplitScreenListener.INSTANCE.getNoCreate() - .getRunningSplitTaskIds(); - return assignTargetsForSplitScreen(targets, splitIds); - } - - /** - * Assigns the provided splitIDs to the {@link #mRemoteTargetHandles}, with index 0 will being - * the left/top task, index 1 right/bottom - */ - public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets, - int[] splitIds) { - RemoteAnimationTargetCompat topLeftTarget; // only one set if single/fullscreen task - RemoteAnimationTargetCompat bottomRightTarget; if (mRemoteTargetHandles.length == 1) { // If we're not in split screen, the splitIds count doesn't really matter since we // should always hit this case. mRemoteTargetHandles[0].mTransformParams.setTargetSet(targets); if (targets.apps.length > 0) { // Unclear why/when target.apps length == 0, but it sure does happen :( - topLeftTarget = targets.apps[0]; - mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(topLeftTarget, null); + mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(targets.apps[0], null); } } else { - // split screen - topLeftTarget = targets.findTask(splitIds[0]); - bottomRightTarget = targets.findTask(splitIds[1]); + RemoteAnimationTarget topLeftTarget = targets.apps[0]; + + // Fetch the adjacent target for split screen. + RemoteAnimationTarget bottomRightTarget = null; + for (int i = 1; i < targets.apps.length; i++) { + final RemoteAnimationTarget target = targets.apps[i]; + Rect topLeftBounds = getStartBounds(topLeftTarget); + Rect bounds = getStartBounds(target); + if (topLeftBounds.left > bounds.right || topLeftBounds.top > bounds.bottom) { + bottomRightTarget = topLeftTarget; + topLeftTarget = target; + break; + } else if (topLeftBounds.right < bounds.left || topLeftBounds.bottom < bounds.top) { + bottomRightTarget = target; + break; + } + } // remoteTargetHandle[0] denotes topLeft task, so we pass in the bottomRight to exclude, // vice versa - mStagedSplitBounds = new StagedSplitBounds( - topLeftTarget.screenSpaceBounds, - bottomRightTarget.screenSpaceBounds, splitIds[0], splitIds[1]); + mSplitBounds = new SplitBounds( + getStartBounds(topLeftTarget), + getStartBounds(bottomRightTarget), + topLeftTarget.taskId, + bottomRightTarget.taskId); mRemoteTargetHandles[0].mTransformParams.setTargetSet( createRemoteAnimationTargetsForTarget(targets, bottomRightTarget)); mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(topLeftTarget, - mStagedSplitBounds); + mSplitBounds); mRemoteTargetHandles[1].mTransformParams.setTargetSet( createRemoteAnimationTargetsForTarget(targets, topLeftTarget)); mRemoteTargetHandles[1].mTaskViewSimulator.setPreview(bottomRightTarget, - mStagedSplitBounds); + mSplitBounds); } return mRemoteTargetHandles; } + /** + * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this creates distinct + * transform params per app in {@code targets.apps} list. + */ + public RemoteTargetHandle[] assignTargetsForDesktop(RemoteAnimationTargets targets) { + for (int i = 0; i < mRemoteTargetHandles.length; i++) { + RemoteAnimationTarget primaryTaskTarget = targets.apps[i]; + mRemoteTargetHandles[i].mTransformParams.setTargetSet( + createRemoteAnimationTargetsForTaskId(targets, primaryTaskTarget.taskId)); + mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null); + } + return mRemoteTargetHandles; + } + + private Rect getStartBounds(RemoteAnimationTarget target) { + return target.startBounds == null ? target.screenSpaceBounds : target.startBounds; + } + /** * Ensures that we aren't excluding ancillary targets such as home/recents * @@ -144,28 +182,62 @@ public class RemoteTargetGluer { */ private RemoteAnimationTargets createRemoteAnimationTargetsForTarget( RemoteAnimationTargets targets, - @Nullable RemoteAnimationTargetCompat targetToExclude) { - int finalLength = targets.unfilteredApps.length - (targetToExclude == null ? 0 : 1); - RemoteAnimationTargetCompat[] targetsWithoutExcluded = - new RemoteAnimationTargetCompat[finalLength]; - int i = 0; - for (RemoteAnimationTargetCompat targetCompat : targets.unfilteredApps) { + RemoteAnimationTarget targetToExclude) { + ArrayList targetsWithoutExcluded = new ArrayList<>(); + + for (RemoteAnimationTarget targetCompat : targets.unfilteredApps) { if (targetCompat == targetToExclude) { continue; } - targetsWithoutExcluded[i] = targetCompat; - i++; + if (targetToExclude != null + && targetToExclude.taskInfo != null + && targetCompat.taskInfo != null + && targetToExclude.taskInfo.parentTaskId == targetCompat.taskInfo.taskId) { + // Also exclude corresponding parent task + continue; + } + + targetsWithoutExcluded.add(targetCompat); } - return new RemoteAnimationTargets(targetsWithoutExcluded, - targets.wallpapers, targets.nonApps, targets.targetMode); + final RemoteAnimationTarget[] filteredApps = targetsWithoutExcluded.toArray( + new RemoteAnimationTarget[targetsWithoutExcluded.size()]); + return new RemoteAnimationTargets( + filteredApps, targets.wallpapers, targets.nonApps, targets.targetMode); + } + + /** + * Ensures that we only animate one specific app target. Includes ancillary targets such as + * home/recents + * + * @param targets remote animation targets to filter + * @param taskId id for a task that we want this remote animation to apply to + * @return {@link RemoteAnimationTargets} where app target only includes the app that has the + * {@code taskId} that was passed in + */ + private RemoteAnimationTargets createRemoteAnimationTargetsForTaskId( + RemoteAnimationTargets targets, int taskId) { + RemoteAnimationTarget[] targetApp = null; + for (RemoteAnimationTarget targetCompat : targets.unfilteredApps) { + if (targetCompat.taskId == taskId) { + targetApp = new RemoteAnimationTarget[]{targetCompat}; + break; + } + } + + if (targetApp == null) { + targetApp = new RemoteAnimationTarget[0]; + } + + return new RemoteAnimationTargets(targetApp, targets.wallpapers, targets.nonApps, + targets.targetMode); } public RemoteTargetHandle[] getRemoteTargetHandles() { return mRemoteTargetHandles; } - public StagedSplitBounds getStagedSplitBounds() { - return mStagedSplitBounds; + public SplitBounds getSplitBounds() { + return mSplitBounds; } /** diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java index 479a661272..f5db2444f8 100644 --- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java +++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java @@ -20,20 +20,23 @@ import static android.view.Surface.ROTATION_0; import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN; import static com.android.launcher3.util.DisplayController.CHANGE_ALL; +import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; +import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS; +import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS; import android.content.Context; import android.content.res.Resources; import android.view.MotionEvent; import android.view.OrientationEventListener; -import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.NavigationMode; import com.android.quickstep.util.RecentsOrientedState; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.TaskStackChangeListener; @@ -44,22 +47,22 @@ import java.util.ArrayList; import app.lawnchair.util.LawnchairUtilsKt; -public class RotationTouchHelper implements - SysUINavigationMode.NavigationModeChangeListener, - DisplayInfoChangeListener { +/** + * Helper class for transforming touch events + */ +public class RotationTouchHelper implements DisplayInfoChangeListener { - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(RotationTouchHelper::new); + public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>( + RotationTouchHelper::new); private OrientationTouchTransformer mOrientationTouchTransformer; private DisplayController mDisplayController; - private SysUINavigationMode mSysUiNavMode; private int mDisplayId; private int mDisplayRotation; private final ArrayList mOnDestroyActions = new ArrayList<>(); - private SysUINavigationMode.Mode mMode = THREE_BUTTONS; + private NavigationMode mMode = THREE_BUTTONS; private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() { @Override @@ -73,9 +76,10 @@ public class RotationTouchHelper implements @Override public void onActivityRotation(int displayId) { - super.onActivityRotation(displayId); - // This always gets called before onDisplayInfoChanged() so we know how to process - // the rotation in that method. This is done to avoid having a race condition between + // This always gets called before onDisplayInfoChanged() so we know how to + // process + // the rotation in that method. This is done to avoid having a race condition + // between // the sensor readings and onDisplayInfoChanged() call if (displayId != mDisplayId) { return; @@ -98,29 +102,38 @@ public class RotationTouchHelper implements }; /** - * Used to listen for when the device rotates into the orientation of the current foreground - * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then - * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust + * Used to listen for when the device rotates into the orientation of the + * current foreground + * app. For example, if a user quickswitches from a portrait to a fixed + * landscape app and then + * rotates rotates the device to match that orientation, this triggers calls to + * sysui to adjust * the navbar. */ private OrientationEventListener mOrientationListener; private int mSensorRotation = ROTATION_0; /** - * This is the configuration of the foreground app or the app that will be in the foreground + * This is the configuration of the foreground app or the app that will be in + * the foreground * once a quickstep gesture finishes. */ private int mCurrentAppRotation = -1; /** - * This flag is set to true when the device physically changes orientations. When true, we will - * always report the current rotation of the foreground app whenever the display changes, as it + * This flag is set to true when the device physically changes orientations. + * When true, we will + * always report the current rotation of the foreground app whenever the display + * changes, as it * would indicate the user's intention to rotate the foreground app. */ private boolean mPrioritizeDeviceRotation = false; private Runnable mOnDestroyFrozenTaskRunnable; /** - * Set to true when user swipes to recents. In recents, we ignore the state of the recents - * task list being frozen or not to allow the user to keep interacting with nav bar rotation - * they went into recents with as opposed to defaulting to the default display rotation. + * Set to true when user swipes to recents. In recents, we ignore the state of + * the recents + * task list being frozen or not to allow the user to keep interacting with nav + * bar rotation + * they went into recents with as opposed to defaulting to the default display + * rotation. * TODO: (b/156984037) For when user rotates after entering overview */ private boolean mInOverview; @@ -128,8 +141,10 @@ public class RotationTouchHelper implements private final Context mContext; /** - * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests - * where multiple instances of RotationTouchHelper are being created. b/177316094 + * Keeps track of whether destroy has been called for this instance. Mainly used + * for TAPL tests + * where multiple instances of RotationTouchHelper are being created. + * b/177316094 */ private boolean mNeedsInit = true; @@ -146,16 +161,16 @@ public class RotationTouchHelper implements } mDisplayController = DisplayController.INSTANCE.get(mContext); Resources resources = mContext.getResources(); - mSysUiNavMode = SysUINavigationMode.INSTANCE.get(mContext); mDisplayId = DEFAULT_DISPLAY; mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode, () -> LawnchairUtilsKt.getWindowCornerRadius(mContext)); // Register for navigation mode changes - SysUINavigationMode.Mode newMode = mSysUiNavMode.addModeChangeListener(this); - onNavModeChangedInternal(newMode, newMode.hasGestures); - runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this)); + mDisplayController.addChangeListener(this); + DisplayController.Info info = mDisplayController.getInfo(); + onDisplayInfoChangedInternal(info, CHANGE_ALL, info.navigationMode.hasGestures); + runOnDestroy(() -> mDisplayController.removeChangeListener(this)); mOrientationListener = new OrientationEventListener(mContext) { @Override @@ -182,7 +197,7 @@ public class RotationTouchHelper implements void onUserUnlocked() { // We can't load custom window radius before the user had unlocked, // so we just fallback to system values and then reload it here. - onNavigationModeChanged(mSysUiNavMode.getMode()); +// onNavigationModeChanged(mSysUiNavMode.getMode()); } private void setupOrientationSwipeHandler() { @@ -224,7 +239,8 @@ public class RotationTouchHelper implements } /** - * Updates the regions for detecting the swipe up/quickswitch and assistant gestures. + * Updates the regions for detecting the swipe up/quickswitch and assistant + * gestures. */ public void updateGestureTouchRegions() { if (!mMode.hasGestures) { @@ -235,14 +251,16 @@ public class RotationTouchHelper implements } /** - * @return whether the coordinates of the {@param event} is in the swipe up gesture region. + * @return whether the coordinates of the {@param event} is in the swipe up + * gesture region. */ public boolean isInSwipeUpTouchRegion(MotionEvent event) { return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()); } /** - * @return whether the coordinates of the {@param event} with the given {@param pointerIndex} + * @return whether the coordinates of the {@param event} with the given + * {@param pointerIndex} * is in the swipe up gesture region. */ public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) { @@ -250,66 +268,58 @@ public class RotationTouchHelper implements event.getY(pointerIndex)); } - @Override - public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) { - onNavModeChangedInternal(newMode, false); + public void onDisplayInfoChanged(Context context, Info info, int flags) { + onDisplayInfoChangedInternal(info, flags, false); } - /** - * @param forceRegister if {@code true}, this will register {@link #mFrozenTaskListener} via - * {@link #setupOrientationSwipeHandler()} - */ - private void onNavModeChangedInternal(SysUINavigationMode.Mode newMode, boolean forceRegister) { - mDisplayController.removeChangeListener(this); - mDisplayController.addChangeListener(this); - onDisplayInfoChanged(mContext, mDisplayController.getInfo(), CHANGE_ALL); + private void onDisplayInfoChangedInternal(Info info, int flags, boolean forceRegister) { + if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN | CHANGE_NAVIGATION_MODE + | CHANGE_SUPPORTED_BOUNDS)) != 0) { + mDisplayRotation = info.rotation; - mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(), - mContext.getResources()); + if (mMode.hasGestures) { + updateGestureTouchRegions(); + mOrientationTouchTransformer.createOrAddTouchRegion(info); + mCurrentAppRotation = mDisplayRotation; - if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) { - setupOrientationSwipeHandler(); - } else if (mMode.hasGestures && !newMode.hasGestures){ - destroyOrientationSwipeHandlerCallback(); + /* + * Update nav bars on the following: + * a) if this is coming from an activity rotation OR + * aa) we launch an app in the orientation that user is already in + * b) We're not in overview, since overview will always be portrait (w/o home + * rotation) + * c) We're actively in quickswitch mode + */ + if ((mPrioritizeDeviceRotation + || mCurrentAppRotation == mSensorRotation) + // switch to an app of orientation user is in + && !mInOverview + && mTaskListFrozen) { + toggleSecondaryNavBarsForRotation(); + } + } } - mMode = newMode; + if ((flags & CHANGE_NAVIGATION_MODE) != 0) { + NavigationMode newMode = info.navigationMode; + mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(), + mContext.getResources()); + + if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) { + setupOrientationSwipeHandler(); + } else if (mMode.hasGestures && !newMode.hasGestures) { + destroyOrientationSwipeHandlerCallback(); + } + + mMode = newMode; + } } public int getDisplayRotation() { return mDisplayRotation; } - @Override - public void onDisplayInfoChanged(Context context, Info info, int flags) { - if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN)) == 0) { - return; - } - - mDisplayRotation = info.rotation; - - if (!mMode.hasGestures) { - return; - } - updateGestureTouchRegions(); - mOrientationTouchTransformer.createOrAddTouchRegion(info); - mCurrentAppRotation = mDisplayRotation; - - /* Update nav bars on the following: - * a) if this is coming from an activity rotation OR - * aa) we launch an app in the orientation that user is already in - * b) We're not in overview, since overview will always be portrait (w/o home rotation) - * c) We're actively in quickswitch mode - */ - if ((mPrioritizeDeviceRotation - || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in - && !mInOverview - && mTaskListFrozen) { - toggleSecondaryNavBarsForRotation(); - } - } - /** * Sets the gestural height. */ @@ -319,7 +329,8 @@ public class RotationTouchHelper implements } /** - * *May* apply a transform on the motion event if it lies in the nav bar region for another + * *May* apply a transform on the motion event if it lies in the nav bar region + * for another * orientation that is currently being tracked as a part of quickstep */ void setOrientationTransformIfNeeded(MotionEvent event) { @@ -336,9 +347,9 @@ public class RotationTouchHelper implements if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) { // Clear any previous state from sensor manager mSensorRotation = mCurrentAppRotation; - mOrientationListener.enable(); + UI_HELPER_EXECUTOR.execute(mOrientationListener::enable); } else { - mOrientationListener.disable(); + UI_HELPER_EXECUTOR.execute(mOrientationListener::disable); } } @@ -372,7 +383,8 @@ public class RotationTouchHelper implements } // A new gesture is starting, reset the current device rotation - // This is done under the assumption that the user won't rotate the phone and then + // This is done under the assumption that the user won't rotate the phone and + // then // quickswitch in the old orientation. mPrioritizeDeviceRotation = false; } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) { @@ -390,7 +402,8 @@ public class RotationTouchHelper implements } /** - * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then + * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and + * then * notifies system UI of the primary rotation the user is interacting with */ private void toggleSecondaryNavBarsForRotation() { diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java index 8e9b668c15..f913aff167 100644 --- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java +++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java @@ -17,7 +17,6 @@ package com.android.quickstep; import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_SELECT; import android.animation.Animator; import android.content.Context; @@ -25,25 +24,27 @@ import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; import android.graphics.Rect; import android.graphics.RectF; +import android.view.RemoteAnimationTarget; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; import com.android.quickstep.util.AnimatorControllerWithResistance; -import com.android.quickstep.util.LauncherSplitScreenListener; import com.android.quickstep.util.RectFSpringAnim; +import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig; +import com.android.quickstep.util.RectFSpringAnim.TaskbarHotseatSpringConfig; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.TransformParams.BuilderProxy; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder; import java.util.Arrays; import java.util.function.Consumer; @@ -67,23 +68,21 @@ public abstract class SwipeUpAnimationLogic implements // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely // visible. protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); + protected float mCurrentDisplacement; // The distance needed to drag to reach the task size in recents. protected int mTransitionDragLength; // How much further we can drag past recents, as a factor of mTransitionDragLength. protected float mDragLengthFactor = 1; - protected boolean mIsSwipeForStagedSplit; + protected boolean mIsSwipeForSplit; public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState, GestureState gestureState) { mContext = context; mDeviceState = deviceState; mGestureState = gestureState; - - mIsSwipeForStagedSplit = ENABLE_SPLIT_SELECT.get() && - LauncherSplitScreenListener.INSTANCE.getNoCreate() - .getRunningSplitTaskIds().length > 1; + mIsSwipeForSplit = TopTaskTracker.INSTANCE.get(context).getRunningSplitTaskIds().length > 1; mTargetGluer = new RemoteTargetGluer(mContext, mGestureState.getActivityInterface()); mRemoteTargetHandles = mTargetGluer.getRemoteTargetHandles(); @@ -120,7 +119,9 @@ public abstract class SwipeUpAnimationLogic implements @UiThread public void updateDisplacement(float displacement) { // We are moving in the negative x/y direction - displacement = -displacement; + displacement = overrideDisplacementForTransientTaskbar(-displacement); + mCurrentDisplacement = displacement; + float shift; if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) { shift = mDragLengthFactor; @@ -132,6 +133,17 @@ public abstract class SwipeUpAnimationLogic implements mCurrentShift.updateValue(shift); } + /** + * When Transient Taskbar is enabled, subclasses can override the displacement to keep the app + * window at the bottom of the screen while taskbar is being swiped in. + * @param displacement The distance the user has swiped up from the bottom of the screen. This + * value will be positive unless the user swipe downwards. + * @return the overridden displacement. + */ + protected float overrideDisplacementForTransientTaskbar(float displacement) { + return displacement; + } + /** * Called when the value of {@link #mCurrentShift} changes */ @@ -147,6 +159,13 @@ public abstract class SwipeUpAnimationLogic implements protected abstract class HomeAnimationFactory { protected float mSwipeVelocity; + /** + * Returns true if we know the home animation involves an item in the hotseat. + */ + public boolean isInHotseat() { + return false; + } + public @NonNull RectF getWindowTargetRect() { PagedOrientationHandler orientationHandler = getOrientationHandler(); DeviceProfile dp = mDp; @@ -177,24 +196,12 @@ public abstract class SwipeUpAnimationLogic implements // No-op } - public boolean shouldPlayAtomicWorkspaceReveal() { - return true; - } - public void setAnimation(RectFSpringAnim anim) { } public void update(RectF currentRect, float progress, float radius) { } public void onCancel() { } - /** - * @return {@code true} if this factory supports animating an Activity to PiP window on - * swiping up to home. - */ - public boolean supportSwipePipToHome() { - return false; - } - /** * @param progress The progress of the animation to the home screen. * @return The current alpha to set on the animating app window. @@ -280,10 +287,21 @@ public abstract class SwipeUpAnimationLogic implements RectF cropRectF = new RectF(taskViewSimulator.getCurrentCropRect()); // Move the startRect to Launcher space as floatingIconView runs in Launcher Matrix windowToHomePositionMap = new Matrix(); + + // If the start rect ends up overshooting too much to the left/right offscreen, bring it + // back to fullscreen. This can happen when the recentsScroll value isn't aligned with + // the pageScroll value for a given taskView, see b/228829958#comment12 + mRemoteTargetHandles[0].getTaskViewSimulator().getOrientationState().getOrientationHandler() + .fixBoundsForHomeAnimStartRect(startRect, mDp); + homeToWindowPositionMap.invert(windowToHomePositionMap); windowToHomePositionMap.mapRect(startRect); - RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext, mDp); + boolean useTaskbarHotseatParams = mDp.isTaskbarPresent + && homeAnimationFactory.isInHotseat(); + RectFSpringAnim anim = new RectFSpringAnim(useTaskbarHotseatParams + ? new TaskbarHotseatSpringConfig(mContext, startRect, targetRect) + : new DefaultSpringConfig(mContext, mDp, startRect, targetRect)); homeAnimationFactory.setAnimation(anim); SpringAnimationRunner runner = new SpringAnimationRunner( @@ -345,11 +363,11 @@ public abstract class SwipeUpAnimationLogic implements } @Override - public void onBuildTargetParams( - Builder builder, RemoteAnimationTargetCompat app, TransformParams params) { - builder.withMatrix(mMatrix) - .withWindowCrop(mCropRect) - .withCornerRadius(params.getCornerRadius()); + public void onBuildTargetParams(SurfaceProperties builder, RemoteAnimationTarget app, + TransformParams params) { + builder.setMatrix(mMatrix) + .setWindowCrop(mCropRect) + .setCornerRadius(params.getCornerRadius()); } @Override diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java deleted file mode 100644 index c584ca9cda..0000000000 --- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep; - -import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON; -import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.android.launcher3.Utilities; -import com.android.launcher3.ResourceUtils; -import com.android.launcher3.logging.StatsLogManager.LauncherEvent; -import com.android.launcher3.util.MainThreadInitializedObject; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Observer for the resource config that specifies the navigation bar mode. - */ -public class SysUINavigationMode { - - public enum Mode { - THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON), - TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON), - NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON); - - public final boolean hasGestures; - public final int resValue; - public final LauncherEvent launcherEvent; - - Mode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) { - this.hasGestures = hasGestures; - this.resValue = resValue; - this.launcherEvent = launcherEvent; - } - } - - public static Mode getMode(Context context) { - return INSTANCE.get(context).getMode(); - } - - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(SysUINavigationMode::new); - - private static final String TAG = "SysUINavigationMode"; - private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; - private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = - "config_navBarInteractionMode"; - private static final String TARGET_OVERLAY_PACKAGE = "android"; - - private final Context mContext; - private Mode mMode; - - private int mNavBarGesturalHeight; - private int mNavBarLargerGesturalHeight; - - private final List mChangeListeners = - new CopyOnWriteArrayList<>(); - private final List mOneHandedOverlayChangeListeners = - new ArrayList<>(); - - public SysUINavigationMode(Context context) { - mContext = context; - initializeMode(); - - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateMode(); - updateGesturalHeight(); - } - }, getPackageFilter(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED)); - - if (!Utilities.ATLEAST_Q) { - mMode = Mode.THREE_BUTTONS; - } - } - - /** Updates navigation mode when needed. */ - public void updateMode() { - Mode oldMode = mMode; - initializeMode(); - if (mMode != oldMode) { - dispatchModeChange(); - } - } - - private void updateGesturalHeight() { - int newGesturalHeight = ResourceUtils.getDimenByName( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(), - INVALID_RESOURCE_HANDLE); - - if (newGesturalHeight == INVALID_RESOURCE_HANDLE) { - Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); - return; - } - - if (mNavBarGesturalHeight != newGesturalHeight) { - mNavBarGesturalHeight = newGesturalHeight; - } - - int newLargerGesturalHeight = ResourceUtils.getDimenByName( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, mContext.getResources(), - INVALID_RESOURCE_HANDLE); - if (newLargerGesturalHeight == INVALID_RESOURCE_HANDLE) { - Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); - return; - } - if (mNavBarLargerGesturalHeight != newLargerGesturalHeight) { - mNavBarLargerGesturalHeight = newLargerGesturalHeight; - dispatchOneHandedOverlayChange(); - } - } - - private void initializeMode() { - mMode = Mode.THREE_BUTTONS; - - int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME, - mContext.getResources(), INVALID_RESOURCE_HANDLE); - mNavBarGesturalHeight = ResourceUtils.getDimenByName( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(), - INVALID_RESOURCE_HANDLE); - mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, mContext.getResources(), - mNavBarGesturalHeight); - - if (modeInt == INVALID_RESOURCE_HANDLE) { - Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); - return; - } - - for (Mode m : Mode.values()) { - if (m.resValue == modeInt) { - mMode = m; - } - } - } - - private void dispatchModeChange() { - for (NavigationModeChangeListener listener : mChangeListeners) { - listener.onNavigationModeChanged(mMode); - } - } - - private void dispatchOneHandedOverlayChange() { - for (OneHandedModeChangeListener listener : mOneHandedOverlayChangeListeners) { - listener.onOneHandedModeChanged(mNavBarLargerGesturalHeight); - } - } - - public Mode addModeChangeListener(NavigationModeChangeListener listener) { - mChangeListeners.add(listener); - return mMode; - } - - public void removeModeChangeListener(NavigationModeChangeListener listener) { - mChangeListeners.remove(listener); - } - - public int addOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) { - mOneHandedOverlayChangeListeners.add(listener); - return mNavBarLargerGesturalHeight; - } - - public void removeOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) { - mOneHandedOverlayChangeListeners.remove(listener); - } - - public Mode getMode() { - return mMode; - } - - public void dump(PrintWriter pw) { - pw.println("SysUINavigationMode:"); - pw.println(" mode=" + mMode.name()); - pw.println(" mNavBarGesturalHeight=:" + mNavBarGesturalHeight); - } - - public interface NavigationModeChangeListener { - void onNavigationModeChanged(Mode newMode); - } - - public interface OneHandedModeChangeListener { - void onOneHandedModeChanged(int newGesturalHeight); - } -} diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index b2affa2145..b2ee376a75 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -18,19 +18,23 @@ package com.android.quickstep; import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.app.ActivityManager; import android.app.PendingIntent; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.graphics.Bitmap; -import android.graphics.Insets; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; +import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -38,15 +42,25 @@ import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.window.IOnBackInvokedCallback; +import android.window.RemoteTransition; +import android.window.TransitionFilter; -import com.android.launcher3.Utilities; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import com.android.internal.logging.InstanceId; +import com.android.internal.util.ScreenshotRequest; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.systemui.shared.recents.ISystemUiProxy; -import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.RemoteTransitionCompat; -import com.android.systemui.shared.system.smartspace.ISmartspaceCallback; -import com.android.systemui.shared.system.smartspace.ISmartspaceTransitionController; +import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController; +import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController; +import com.android.systemui.shared.system.smartspace.SmartspaceState; +import com.android.systemui.unfold.progress.IUnfoldAnimation; +import com.android.systemui.unfold.progress.IUnfoldTransitionListener; +import com.android.wm.shell.back.IBackAnimation; +import com.android.wm.shell.desktopmode.IDesktopMode; import com.android.wm.shell.onehanded.IOneHanded; import com.android.wm.shell.pip.IPip; import com.android.wm.shell.pip.IPipAnimationListener; @@ -61,59 +75,68 @@ import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; /** * Holds the reference to SystemUI. */ -public class SystemUiProxy implements ISystemUiProxy, - SysUINavigationMode.NavigationModeChangeListener { +public class SystemUiProxy implements ISystemUiProxy { private static final String TAG = SystemUiProxy.class.getSimpleName(); - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(SystemUiProxy::new); + public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>( + SystemUiProxy::new); + + private static final int MSG_SET_SHELF_HEIGHT = 1; + private static final int MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT = 2; private ISystemUiProxy mSystemUiProxy; private IPip mPip; - private ISmartspaceTransitionController mSmartspaceTransitionController; + private ISysuiUnlockAnimationController mSysuiUnlockAnimationController; private ISplitScreen mSplitScreen; private IOneHanded mOneHanded; private IShellTransitions mShellTransitions; private IStartingWindow mStartingWindow; private IRecentTasks mRecentTasks; + private IBackAnimation mBackAnimation; + private IDesktopMode mDesktopMode; + private IUnfoldAnimation mUnfoldAnimation; private final DeathRecipient mSystemUiProxyDeathRecipient = () -> { MAIN_EXECUTOR.execute(() -> clearProxy()); }; - // Save the listeners passed into the proxy since OverviewProxyService may not have been bound - // yet, and we'll need to set/register these listeners with SysUI when they do. Note that it is - // up to the caller to clear the listeners to prevent leaks as these can be held indefinitely + // Save the listeners passed into the proxy since OverviewProxyService may not + // have been bound + // yet, and we'll need to set/register these listeners with SysUI when they do. + // Note that it is + // up to the caller to clear the listeners to prevent leaks as these can be held + // indefinitely // in case SysUI needs to rebind. private IPipAnimationListener mPipAnimationListener; private ISplitScreenListener mSplitScreenListener; private IStartingWindowListener mStartingWindowListener; - private ISmartspaceCallback mSmartspaceCallback; + private ILauncherUnlockAnimationController mLauncherUnlockAnimationController; private IRecentTasksListener mRecentTasksListener; - private final ArrayList mRemoteTransitions = new ArrayList<>(); + private IUnfoldTransitionListener mUnfoldAnimationListener; + private final LinkedHashMap mRemoteTransitions = new LinkedHashMap<>(); + private IOnBackInvokedCallback mBackToLauncherCallback; // Used to dedupe calls to SystemUI private int mLastShelfHeight; private boolean mLastShelfVisible; - private float mLastNavButtonAlpha; - private boolean mLastNavButtonAnimate; - private boolean mHasNavButtonAlphaBeenSet = false; - private Runnable mPendingSetNavButtonAlpha = null; + + // Used to dedupe calls to SystemUI + private int mLastLauncherKeepClearAreaHeight; + private boolean mLastLauncherKeepClearAreaHeightVisible; + + private final Context mContext; + private final Handler mAsyncHandler; // TODO(141886704): Find a way to remove this private int mLastSystemUiStateFlags; public SystemUiProxy(Context context) { - SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this); - } - - @Override - public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) { - // Whenever the nav mode changes, force reset the nav button alpha - setNavBarButtonAlpha(1f, false); + mContext = context; + mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync); } @Override @@ -158,7 +181,9 @@ public class SystemUiProxy implements ISystemUiProxy, public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen, IOneHanded oneHanded, IShellTransitions shellTransitions, IStartingWindow startingWindow, IRecentTasks recentTasks, - ISmartspaceTransitionController smartSpaceTransitionController) { + ISysuiUnlockAnimationController sysuiUnlockAnimationController, + IBackAnimation backAnimation, IDesktopMode desktopMode, + IUnfoldAnimation unfoldAnimation) { unlinkToDeath(); mSystemUiProxy = proxy; mPip = pip; @@ -166,12 +191,16 @@ public class SystemUiProxy implements ISystemUiProxy, mOneHanded = oneHanded; mShellTransitions = shellTransitions; mStartingWindow = startingWindow; - mSmartspaceTransitionController = smartSpaceTransitionController; + mSysuiUnlockAnimationController = sysuiUnlockAnimationController; mRecentTasks = recentTasks; + mBackAnimation = backAnimation; + mDesktopMode = desktopMode; + mUnfoldAnimation = unfoldAnimation; linkToDeath(); - // re-attach the listeners once missing due to setProxy has not been initialized yet. + // re-attach the listeners once missing due to setProxy has not been initialized + // yet. if (mPipAnimationListener != null && mPip != null) { - setPinnedStackAnimationListener(mPipAnimationListener); + setPipAnimationListener(mPipAnimationListener); } if (mSplitScreenListener != null && mSplitScreen != null) { registerSplitScreenListener(mSplitScreenListener); @@ -179,24 +208,23 @@ public class SystemUiProxy implements ISystemUiProxy, if (mStartingWindowListener != null && mStartingWindow != null) { setStartingWindowListener(mStartingWindowListener); } - if (mSmartspaceCallback != null && mSmartspaceTransitionController != null) { - setSmartspaceCallback(mSmartspaceCallback); - } - for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) { - registerRemoteTransition(mRemoteTransitions.get(i)); + if (mSysuiUnlockAnimationController != null && mLauncherUnlockAnimationController != null) { + setLauncherUnlockAnimationController(mLauncherUnlockAnimationController); } + new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition); if (mRecentTasksListener != null && mRecentTasks != null) { registerRecentTasksListener(mRecentTasksListener); } - - if (mPendingSetNavButtonAlpha != null) { - mPendingSetNavButtonAlpha.run(); - mPendingSetNavButtonAlpha = null; + if (mBackAnimation != null && mBackToLauncherCallback != null) { + setBackToLauncherCallback(mBackToLauncherCallback); + } + if (unfoldAnimation != null && mUnfoldAnimationListener != null) { + setUnfoldAnimationListener(mUnfoldAnimationListener); } } public void clearProxy() { - setProxy(null, null, null, null, null, null, null, null); + setProxy(null, null, null, null, null, null, null, null, null, null, null); } // TODO(141886704): Find a way to remove this @@ -255,43 +283,6 @@ public class SystemUiProxy implements ISystemUiProxy, } } - @Override - public Rect getNonMinimizedSplitScreenSecondaryBounds() { - if (mSystemUiProxy != null) { - try { - return mSystemUiProxy.getNonMinimizedSplitScreenSecondaryBounds(); - } catch (RemoteException e) { - Log.w(TAG, "Failed call getNonMinimizedSplitScreenSecondaryBounds", e); - } - } - return null; - } - - public float getLastNavButtonAlpha() { - return mLastNavButtonAlpha; - } - - @Override - public void setNavBarButtonAlpha(float alpha, boolean animate) { - boolean changed = Float.compare(alpha, mLastNavButtonAlpha) != 0 - || animate != mLastNavButtonAnimate - || !mHasNavButtonAlphaBeenSet; - if (changed) { - if (mSystemUiProxy == null) { - mPendingSetNavButtonAlpha = () -> setNavBarButtonAlpha(alpha, animate); - } else { - mLastNavButtonAlpha = alpha; - mLastNavButtonAnimate = animate; - mHasNavButtonAlphaBeenSet = true; - try { - mSystemUiProxy.setNavBarButtonAlpha(alpha, animate); - } catch (RemoteException e) { - Log.w(TAG, "Failed call setNavBarButtonAlpha", e); - } - } - } - } - @Override public void onStatusBarMotionEvent(MotionEvent event) { if (mSystemUiProxy != null) { @@ -336,17 +327,6 @@ public class SystemUiProxy implements ISystemUiProxy, } } - @Override - public Bundle monitorGestureInput(String name, int displayId) { - if (mSystemUiProxy != null) { - try { - return mSystemUiProxy.monitorGestureInput(name, displayId); - } catch (RemoteException e) { - Log.w(TAG, "Failed call monitorGestureInput: " + name, e); - } - } - return null; - } @Override public void notifyAccessibilityButtonClicked(int displayId) { @@ -381,53 +361,6 @@ public class SystemUiProxy implements ISystemUiProxy, } } - @Override - public void handleImageAsScreenshot(Bitmap bitmap, Rect rect, Insets insets, int i) { - if (mSystemUiProxy != null) { - try { - mSystemUiProxy.handleImageAsScreenshot(bitmap, rect, insets, i); - } catch (RemoteException e) { - Log.w(TAG, "Failed call handleImageAsScreenshot", e); - } - } - } - - @Override - public void setSplitScreenMinimized(boolean minimized) { - if (mSystemUiProxy != null) { - try { - mSystemUiProxy.setSplitScreenMinimized(minimized); - } catch (RemoteException e) { - Log.w(TAG, "Failed call setSplitScreenMinimized", e); - } - } - } - - @Override - public void notifySwipeUpGestureStarted() { - if (mSystemUiProxy != null) { - try { - mSystemUiProxy.notifySwipeUpGestureStarted(); - } catch (RemoteException e) { - Log.w(TAG, "Failed call notifySwipeUpGestureStarted", e); - } - } - } - - /** - * Notifies that swipe-to-home action is finished. - */ - @Override - public void notifySwipeToHomeFinished() { - if (mSystemUiProxy != null) { - try { - mSystemUiProxy.notifySwipeToHomeFinished(); - } catch (RemoteException e) { - Log.w(TAG, "Failed call notifySwipeToHomeFinished", e); - } - } - } - @Override public void notifyPrioritizedRotation(int rotation) { if (mSystemUiProxy != null) { @@ -439,12 +372,6 @@ public class SystemUiProxy implements ISystemUiProxy, } } - @Override - public void handleImageBundleAsScreenshotR(Bundle screenImageBundle, Rect locationInScreen, - Insets visibleInsets, Task.TaskKey task) { - // just a placeholder - } - public void setTaskbarEnabled(boolean enabled) { if (mSystemUiProxy != null) { try { @@ -469,7 +396,9 @@ public class SystemUiProxy implements ISystemUiProxy, /** * NOTE: If called to suspend, caller MUST call this method to also un-suspend - * @param suspend should be true to stop auto-hide, false to resume normal behavior + * + * @param suspend should be true to stop auto-hide, false to resume normal + * behavior */ @Override public void notifyTaskbarAutohideSuspend(boolean suspend) { @@ -484,19 +413,12 @@ public class SystemUiProxy implements ISystemUiProxy, } @Override - public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen, - Insets visibleInsets, Task.TaskKey task) { + public void takeScreenshot(ScreenshotRequest request) { if (mSystemUiProxy != null) { try { - if (Utilities.ATLEAST_S) { - mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen, - visibleInsets, task); - } else { - mSystemUiProxy.handleImageBundleAsScreenshotR(screenImageBundle, locationInScreen, - visibleInsets, task); - } + mSystemUiProxy.takeScreenshot(request); } catch (RemoteException e) { - Log.w(TAG, "Failed call handleImageBundleAsScreenshot"); + Log.w(TAG, "Failed call takeScreenshot"); } } } @@ -512,6 +434,17 @@ public class SystemUiProxy implements ISystemUiProxy, } } + @Override + public void toggleNotificationPanel() { + if (mSystemUiProxy != null) { + try { + mSystemUiProxy.toggleNotificationPanel(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call toggleNotificationPanel", e); + } + } + } + // // Pip // @@ -520,12 +453,20 @@ public class SystemUiProxy implements ISystemUiProxy, * Sets the shelf height. */ public void setShelfHeight(boolean visible, int shelfHeight) { + Message.obtain(mAsyncHandler, MSG_SET_SHELF_HEIGHT, + visible ? 1 : 0, shelfHeight).sendToTarget(); + } + + @WorkerThread + private void setShelfHeightAsync(int visibleInt, int shelfHeight) { + boolean visible = visibleInt != 0; boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight; - if (mPip != null && changed) { + IPip pip = mPip; + if (pip != null && changed) { mLastShelfVisible = visible; mLastShelfHeight = shelfHeight; try { - mPip.setShelfHeight(visible, shelfHeight); + pip.setShelfHeight(visible, shelfHeight); } catch (RemoteException e) { Log.w(TAG, "Failed call setShelfHeight visible: " + visible + " height: " + shelfHeight, e); @@ -534,12 +475,39 @@ public class SystemUiProxy implements ISystemUiProxy, } /** - * Sets listener to get pinned stack animation callbacks. + * Sets the height of the keep clear area that is going to be reported by + * the Launcher for the Hotseat. */ - public void setPinnedStackAnimationListener(IPipAnimationListener listener) { + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + Message.obtain(mAsyncHandler, MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT, + visible ? 1 : 0, height).sendToTarget(); + } + + @WorkerThread + private void setLauncherKeepClearAreaHeight(int visibleInt, int height) { + boolean visible = visibleInt != 0; + boolean changed = visible != mLastLauncherKeepClearAreaHeightVisible + || height != mLastLauncherKeepClearAreaHeight; + IPip pip = mPip; + if (pip != null && changed) { + mLastLauncherKeepClearAreaHeightVisible = visible; + mLastLauncherKeepClearAreaHeight = height; + try { + pip.setLauncherKeepClearAreaHeight(visible, height); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setLauncherKeepClearAreaHeight visible: " + visible + + " height: " + height, e); + } + } + } + + /** + * Sets listener to get pip animation callbacks. + */ + public void setPipAnimationListener(IPipAnimationListener listener) { if (mPip != null) { try { - mPip.setPinnedStackAnimationListener(listener); + mPip.setPipAnimationListener(listener); } catch (RemoteException e) { Log.w(TAG, "Failed call setPinnedStackAnimationListener", e); } @@ -547,12 +515,18 @@ public class SystemUiProxy implements ISystemUiProxy, mPipAnimationListener = listener; } + /** + * @return Destination bounds of auto-pip animation, {@code null} if the + * animation is not ready. + */ + @Nullable public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, - PictureInPictureParams pictureInPictureParams, int launcherRotation, int shelfHeight) { + PictureInPictureParams pictureInPictureParams, int launcherRotation, + Rect hotseatKeepClearArea) { if (mPip != null) { try { return mPip.startSwipePipToHome(componentName, activityInfo, - pictureInPictureParams, launcherRotation, shelfHeight); + pictureInPictureParams, launcherRotation, hotseatKeepClearArea); } catch (RemoteException e) { Log.w(TAG, "Failed call startSwipePipToHome", e); } @@ -560,17 +534,50 @@ public class SystemUiProxy implements ISystemUiProxy, return null; } - public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, + /** + * Notifies WM Shell that launcher has finished all the animation for swipe to + * home. WM Shell + * can choose to fade out the overlay when entering PIP is finished, and WM + * Shell should be + * responsible for cleaning up the overlay. + */ + public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { if (mPip != null) { try { - mPip.stopSwipePipToHome(componentName, destinationBounds, overlay); + mPip.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay); } catch (RemoteException e) { Log.w(TAG, "Failed call stopSwipePipToHome"); } } } + /** + * Sets the next pip animation type to be the alpha animation. + */ + public void setPipAnimationTypeToAlpha() { + if (mPip != null) { + try { + mPip.setPipAnimationTypeToAlpha(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setPipAnimationTypeToAlpha", e); + } + } + } + + /** + * Sets the app icon size in pixel used by Launcher all apps. + */ + public void setLauncherAppIconSize(int iconSizePx) { + if (mPip != null) { + try { + mPip.setLauncherAppIconSize(iconSizePx); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setLauncherAppIconSize", e); + } + } + } + // // Splitscreen // @@ -598,15 +605,55 @@ public class SystemUiProxy implements ISystemUiProxy, } /** Start multiple tasks in split-screen simultaneously. */ - public void startTasks(int mainTaskId, Bundle mainOptions, int sideTaskId, Bundle sideOptions, - @SplitConfigurationOptions.StagePosition int sidePosition, float splitRatio, - RemoteTransitionCompat remoteTransition) { + public void startTasks(int taskId1, Bundle options1, int taskId2, Bundle options2, + @SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio, + RemoteTransition remoteTransition, InstanceId instanceId) { if (mSystemUiProxy != null) { try { - mSplitScreen.startTasks(mainTaskId, mainOptions, sideTaskId, sideOptions, - sidePosition, splitRatio, remoteTransition.getTransition()); + mSplitScreen.startTasks(taskId1, options1, taskId2, options2, splitPosition, + splitRatio, remoteTransition, instanceId); } catch (RemoteException e) { - Log.w(TAG, "Failed call startTask"); + Log.w(TAG, "Failed call startTasks"); + } + } + } + + public void startIntentAndTask(PendingIntent pendingIntent, Bundle options1, int taskId, + Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition, + float splitRatio, RemoteTransition remoteTransition, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startIntentAndTask(pendingIntent, options1, taskId, options2, + splitPosition, splitRatio, remoteTransition, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startIntentAndTask"); + } + } + } + + public void startIntents(PendingIntent pendingIntent1, Bundle options1, + PendingIntent pendingIntent2, Bundle options2, + @SplitConfigurationOptions.StagePosition int splitPosition, + float splitRatio, RemoteTransition remoteTransition, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startIntents(pendingIntent1, options1, pendingIntent2, options2, + splitPosition, splitRatio, remoteTransition, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startIntents"); + } + } + } + + public void startShortcutAndTask(ShortcutInfo shortcutInfo, Bundle options1, int taskId, + Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition, + float splitRatio, RemoteTransition remoteTransition, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startShortcutAndTask(shortcutInfo, options1, taskId, options2, + splitPosition, splitRatio, remoteTransition, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startShortcutAndTask"); } } } @@ -614,25 +661,73 @@ public class SystemUiProxy implements ISystemUiProxy, /** * Start multiple tasks in split-screen simultaneously. */ - public void startTasksWithLegacyTransition(int mainTaskId, Bundle mainOptions, int sideTaskId, - Bundle sideOptions, @SplitConfigurationOptions.StagePosition int sidePosition, - float splitRatio, RemoteAnimationAdapter adapter) { + public void startTasksWithLegacyTransition(int taskId1, Bundle options1, int taskId2, + Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { if (mSystemUiProxy != null) { try { - mSplitScreen.startTasksWithLegacyTransition(mainTaskId, mainOptions, sideTaskId, - sideOptions, sidePosition, splitRatio, adapter); + mSplitScreen.startTasksWithLegacyTransition(taskId1, options1, taskId2, options2, + splitPosition, splitRatio, adapter, instanceId); } catch (RemoteException e) { Log.w(TAG, "Failed call startTasksWithLegacyTransition"); } } } + public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + Bundle options1, int taskId, Bundle options2, + @SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startIntentAndTaskWithLegacyTransition(pendingIntent, options1, taskId, + options2, splitPosition, splitRatio, adapter, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startIntentAndTaskWithLegacyTransition"); + } + } + } + + public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, Bundle options1, + int taskId, Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startShortcutAndTaskWithLegacyTransition(shortcutInfo, options1, + taskId, options2, splitPosition, splitRatio, adapter, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startShortcutAndTaskWithLegacyTransition"); + } + } + } + + /** + * Starts a pair of intents or shortcuts in split-screen using legacy + * transition. Passing a + * non-null shortcut info means to start the app as a shortcut. + */ + public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, + PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + @Nullable Bundle options2, @SplitConfigurationOptions.StagePosition int sidePosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { + if (mSystemUiProxy != null) { + try { + mSplitScreen.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1, + options1, pendingIntent2, shortcutInfo2, options2, sidePosition, splitRatio, + adapter, instanceId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call startIntentsWithLegacyTransition"); + } + } + } + public void startShortcut(String packageName, String shortcutId, int position, - Bundle options, UserHandle user) { + Bundle options, UserHandle user, InstanceId instanceId) { if (mSplitScreen != null) { try { mSplitScreen.startShortcut(packageName, shortcutId, position, options, - user); + user, instanceId); } catch (RemoteException e) { Log.w(TAG, "Failed call startShortcut"); } @@ -640,10 +735,10 @@ public class SystemUiProxy implements ISystemUiProxy, } public void startIntent(PendingIntent intent, Intent fillInIntent, int position, - Bundle options) { + Bundle options, InstanceId instanceId) { if (mSplitScreen != null) { try { - mSplitScreen.startIntent(intent, fillInIntent, position, options); + mSplitScreen.startIntent(intent, fillInIntent, position, options, instanceId); } catch (RemoteException e) { Log.w(TAG, "Failed call startIntent"); } @@ -661,17 +756,18 @@ public class SystemUiProxy implements ISystemUiProxy, } /** - * Call this when going to recents so that shell can set-up and provide appropriate leashes + * Call this when going to recents so that shell can set-up and provide + * appropriate leashes * for animation (eg. DividerBar). * - * @param cancel true if recents starting is being cancelled. - * @return RemoteAnimationTargets of windows that need to animate but only exist in shell. + * @return RemoteAnimationTargets of windows that need to animate but only exist + * in shell. */ - public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - RemoteAnimationTarget[] apps) { + @Nullable + public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { if (mSplitScreen != null) { try { - return mSplitScreen.onGoingToRecentsLegacy(cancel, apps); + return mSplitScreen.onGoingToRecentsLegacy(apps); } catch (RemoteException e) { Log.w(TAG, "Failed call onGoingToRecentsLegacy"); } @@ -679,6 +775,18 @@ public class SystemUiProxy implements ISystemUiProxy, return null; } + @Nullable + public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { + if (mSplitScreen != null) { + try { + return mSplitScreen.onStartingSplitLegacy(apps); + } catch (RemoteException e) { + Log.w(TAG, "Failed call onStartingSplitLegacy"); + } + } + return null; + } + // // One handed // @@ -707,24 +815,24 @@ public class SystemUiProxy implements ISystemUiProxy, // Remote transitions // - public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) { + public void registerRemoteTransition( + RemoteTransition remoteTransition, TransitionFilter filter) { if (mShellTransitions != null) { try { - mShellTransitions.registerRemote(remoteTransition.getFilter(), - remoteTransition.getTransition()); + mShellTransitions.registerRemote(filter, remoteTransition); } catch (RemoteException e) { Log.w(TAG, "Failed call registerRemoteTransition"); } } - if (!mRemoteTransitions.contains(remoteTransition)) { - mRemoteTransitions.add(remoteTransition); + if (!mRemoteTransitions.containsKey(remoteTransition)) { + mRemoteTransitions.put(remoteTransition, filter); } } - public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) { + public void unregisterRemoteTransition(RemoteTransition remoteTransition) { if (mShellTransitions != null) { try { - mShellTransitions.unregisterRemote(remoteTransition.getTransition()); + mShellTransitions.unregisterRemote(remoteTransition); } catch (RemoteException e) { Log.w(TAG, "Failed call registerRemoteTransition"); } @@ -754,15 +862,46 @@ public class SystemUiProxy implements ISystemUiProxy, // SmartSpace transitions // - public void setSmartspaceCallback(ISmartspaceCallback callback) { - if (mSmartspaceTransitionController != null) { + /** + * Sets the instance of {@link ILauncherUnlockAnimationController} that System + * UI should use to + * control the launcher side of the unlock animation. This will also cause us to + * dispatch the + * current state of the smartspace to System UI (this will subsequently happen + * if the state + * changes). + */ + public void setLauncherUnlockAnimationController( + ILauncherUnlockAnimationController controller) { + if (mSysuiUnlockAnimationController != null) { try { - mSmartspaceTransitionController.setSmartspace(callback); + mSysuiUnlockAnimationController.setLauncherUnlockController(controller); + + if (controller != null) { + controller.dispatchSmartspaceStateToSysui(); + } } catch (RemoteException e) { - Log.w(TAG, "Failed call setStartingWindowListener", e); + Log.w(TAG, "Failed call setLauncherUnlockAnimationController", e); + } + } + + mLauncherUnlockAnimationController = controller; + } + + /** + * Tells System UI that the Launcher's smartspace state has been updated, so + * that it can prepare + * the unlock animation accordingly. + */ + public void notifySysuiSmartspaceStateUpdated(SmartspaceState state) { + if (mSysuiUnlockAnimationController != null) { + try { + mSysuiUnlockAnimationController.onLauncherSmartspaceStateUpdated(state); + } catch (RemoteException e) { + Log.w(TAG, "Failed call notifySysuiSmartspaceStateUpdated", e); + e.printStackTrace(); } } - mSmartspaceCallback = callback; } // @@ -791,15 +930,144 @@ public class SystemUiProxy implements ISystemUiProxy, mRecentTasksListener = null; } + // + // Back navigation transitions + // + + /** Sets the launcher {@link android.window.IOnBackInvokedCallback} to shell */ + public void setBackToLauncherCallback(IOnBackInvokedCallback callback) { + mBackToLauncherCallback = callback; + if (mBackAnimation == null) { + return; + } + try { + mBackAnimation.setBackToLauncherCallback(callback); + } catch (RemoteException e) { + Log.e(TAG, "Failed call setBackToLauncherCallback", e); + } + } + + /** + * Clears the previously registered {@link IOnBackInvokedCallback}. + * + * @param callback The previously registered callback instance. + */ + public void clearBackToLauncherCallback(IOnBackInvokedCallback callback) { + if (mBackToLauncherCallback != callback) { + return; + } + mBackToLauncherCallback = null; + if (mBackAnimation == null) { + return; + } + try { + mBackAnimation.clearBackToLauncherCallback(); + } catch (RemoteException e) { + Log.e(TAG, "Failed call clearBackToLauncherCallback", e); + } + } + + /** + * Notifies shell that all back to launcher animations have finished (including + * the transition + * that plays after the gesture is committed and before the app is closed. + */ + public void onBackToLauncherAnimationFinished() { + if (mBackAnimation != null) { + try { + mBackAnimation.onBackToLauncherAnimationFinished(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call onBackAnimationFinished", e); + } + } + } + public ArrayList getRecentTasks(int numTasks, int userId) { if (mRecentTasks != null) { try { - return new ArrayList<>(Arrays.asList(mRecentTasks.getRecentTasks(numTasks, - RECENT_IGNORE_UNAVAILABLE, userId))); + final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks, + RECENT_IGNORE_UNAVAILABLE, userId); + if (rawTasks == null) { + return new ArrayList<>(); + } + return new ArrayList<>(Arrays.asList(rawTasks)); } catch (RemoteException e) { Log.w(TAG, "Failed call getRecentTasks", e); } } return new ArrayList<>(); } + + /** + * Gets the set of running tasks. + */ + public ArrayList getRunningTasks(int numTasks) { + if (mRecentTasks != null + && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC)) { + try { + return new ArrayList<>(Arrays.asList(mRecentTasks.getRunningTasks(numTasks))); + } catch (RemoteException e) { + Log.w(TAG, "Failed call getRunningTasks", e); + } + } + return new ArrayList<>(); + } + + private boolean handleMessageAsync(Message msg) { + switch (msg.what) { + case MSG_SET_SHELF_HEIGHT: + setShelfHeightAsync(msg.arg1, msg.arg2); + return true; + case MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT: + setLauncherKeepClearAreaHeight(msg.arg1, msg.arg2); + return true; + } + + return false; + } + + // + // Desktop Mode + // + + /** Call shell to show all apps active on the desktop */ + public void showDesktopApps() { + if (mDesktopMode != null) { + try { + mDesktopMode.showDesktopApps(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call showDesktopApps", e); + } + } + } + + /** Call shell to get number of visible freeform tasks */ + public int getVisibleDesktopTaskCount() { + if (mDesktopMode != null) { + try { + return mDesktopMode.getVisibleTaskCount(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call getVisibleDesktopTaskCount", e); + } + } + return 0; + } + + // + // Unfold transition + // + + /** Sets the unfold animation lister to sysui. */ + public void setUnfoldAnimationListener(IUnfoldTransitionListener callback) { + mUnfoldAnimationListener = callback; + if (mUnfoldAnimation == null) { + return; + } + try { + Log.d(TAG, "Registering unfold animation receiver"); + mUnfoldAnimation.setListener(callback); + } catch (RemoteException e) { + Log.e(TAG, "Failed call setUnfoldAnimationListener", e); + } + } } diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java index 82c7c08153..dde5d55de4 100644 --- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java +++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java @@ -15,48 +15,52 @@ */ package com.android.quickstep; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; + import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; +import static com.android.systemui.shared.system.RemoteTransitionCompat.newRemoteTransition; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.SystemProperties; import android.util.Log; import android.view.RemoteAnimationTarget; +import android.window.RemoteTransition; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; +import com.android.quickstep.TopTaskTracker.CachedTaskInfo; +import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.ActivityOptionsCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.RemoteTransitionCompat; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; -import java.util.Arrays; import java.util.HashMap; public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener { public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS + && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); private RecentsAnimationController mController; private RecentsAnimationCallbacks mCallbacks; private RecentsAnimationTargets mTargets; // Temporary until we can hook into gesture state events private GestureState mLastGestureState; - private RemoteAnimationTargetCompat mLastAppearedTaskTarget; + private RemoteAnimationTarget mLastAppearedTaskTarget; private Runnable mLiveTileCleanUpHandler; private Context mCtx; @@ -70,7 +74,7 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn return; } BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityInterface.isInLiveTileMode() + if (activityInterface.isInLiveTileMode() && activityInterface.getCreatedActivity() != null) { RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); if (recentsView != null) { @@ -100,6 +104,9 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn @UiThread public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState, Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "startRecentsAnimation", + /* gestureEvent= */ START_RECENTS_ANIMATION); // Notify if recents animation is still running if (mController != null) { String msg = "New recents animation started before old animation completed"; @@ -150,31 +157,39 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn } @Override - public void onTasksAppeared(RemoteAnimationTargetCompat[] appearedTaskTargets) { - RemoteAnimationTargetCompat appearedTaskTarget = appearedTaskTargets[0]; + public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) { + RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0]; BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); - // Convert appTargets to type RemoteAnimationTarget for all apps except Home app - RemoteAnimationTarget[] nonHomeApps = Arrays.stream(appearedTaskTargets) - .filter(remoteAnimationTarget -> - remoteAnimationTarget.activityType != ACTIVITY_TYPE_HOME) - .map(RemoteAnimationTargetCompat::unwrap) - .toArray(RemoteAnimationTarget[]::new); - RemoteAnimationTarget[] nonAppTargets = - SystemUiProxy.INSTANCE.getNoCreate() - .onGoingToRecentsLegacy(false, nonHomeApps); + for (RemoteAnimationTarget compat : appearedTaskTargets) { + if (compat.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME + && activityInterface.getCreatedActivity() instanceof RecentsActivity) { + // When receive opening home activity while recents is running, enter home + // and dismiss recents. + ((RecentsActivity) activityInterface.getCreatedActivity()).startHome(); + return; + } + } - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityInterface.isInLiveTileMode() + RemoteAnimationTarget[] nonAppTargets = SystemUiProxy.INSTANCE.get(mCtx) + .onStartingSplitLegacy(appearedTaskTargets); + if (nonAppTargets == null) { + nonAppTargets = new RemoteAnimationTarget[0]; + } + if (activityInterface.isInLiveTileMode() && activityInterface.getCreatedActivity() != null) { RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); if (recentsView != null) { recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, appearedTaskTargets, - new RemoteAnimationTargetCompat[0] /* wallpaper */, - RemoteAnimationTargetCompat.wrap(nonAppTargets) /* nonApps */); + new RemoteAnimationTarget[0] /* wallpaper */, + nonAppTargets /* nonApps */); return; } + } else if (nonAppTargets.length > 0) { + TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets /* nonApps */, + true /*shown*/, null /* animatorHandler */); } if (mController != null) { if (mLastAppearedTaskTarget == null @@ -187,18 +202,50 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn } } } + + @Override + public boolean onSwitchToScreenshot(Runnable onFinished) { + if (!activityInterface.isInLiveTileMode() + || activityInterface.getCreatedActivity() == null) { + // No need to switch since tile is already a screenshot. + onFinished.run(); + } else { + final RecentsView recentsView = + activityInterface.getCreatedActivity().getOverviewPanel(); + if (recentsView != null) { + recentsView.switchToScreenshot(onFinished); + } else { + onFinished.run(); + } + } + return true; + } }); final long eventTime = gestureState.getSwipeUpStartTimeMs(); mCallbacks.addListener(gestureState); mCallbacks.addListener(listener); if (ENABLE_SHELL_TRANSITIONS) { - RemoteTransitionCompat transition = new RemoteTransitionCompat(mCallbacks, + RemoteTransition transition = newRemoteTransition(mCallbacks, mController != null ? mController.getController() : null, mCtx.getIApplicationThread()); - Bundle options = ActivityOptionsCompat.makeRemoteTransition(transition) - .setTransientLaunch().toBundle(); - mCtx.startActivity(intent, options); + final ActivityOptions options = ActivityOptions.makeRemoteTransition(transition); + // Allowing to pause Home if Home is top activity and Recents is not Home. So when user + // start home when recents animation is playing, the home activity can be resumed again + // to let the transition controller collect Home activity. + CachedTaskInfo cti = gestureState.getRunningTask(); + boolean homeIsOnTop = cti != null && cti.isHomeTask(); + if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { + if (cti != null && cti.isFreeformTask()) { + // No transient launch when desktop task is on top + homeIsOnTop = true; + } + } + if (!homeIsOnTop) { + options.setTransientLaunch(); + } + options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime); + UI_HELPER_EXECUTOR.execute(() -> mCtx.startActivity(intent, options.toBundle())); } else { UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance() .startRecentsActivity(intent, eventTime, mCallbacks, null, null)); @@ -211,6 +258,7 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn * Continues the existing running recents animation for a new gesture. */ public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation"); mCallbacks.removeListener(mLastGestureState); mLastGestureState = gestureState; mCallbacks.addListener(gestureState); @@ -225,7 +273,7 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn return; } BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityInterface.isInLiveTileMode() + if (activityInterface.isInLiveTileMode() && activityInterface.getCreatedActivity() != null) { RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); if (recentsView != null) { @@ -249,6 +297,8 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn */ public void finishRunningRecentsAnimation(boolean toHome) { if (mController != null) { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "finishRunningRecentsAnimation", toHome); mCallbacks.notifyAnimationCanceled(); Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome ? mController::finishAnimationToHome @@ -281,6 +331,7 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn * Cleans up the recents animation entirely. */ private void cleanUpRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation"); if (mLiveTileCleanUpHandler != null) { mLiveTileCleanUpHandler.run(); mLiveTileCleanUpHandler = null; diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java index c8979a0f6e..ff9cfb791d 100644 --- a/quickstep/src/com/android/quickstep/TaskIconCache.java +++ b/quickstep/src/com/android/quickstep/TaskIconCache.java @@ -18,6 +18,8 @@ package com.android.quickstep; import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED; import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; +import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.content.Context; import android.content.pm.ActivityInfo; @@ -26,7 +28,6 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.UserHandle; import android.text.TextUtils; import android.util.SparseArray; @@ -45,6 +46,7 @@ import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.Preconditions; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.TaskKeyLruCache; +import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.system.PackageManagerWrapper; @@ -66,10 +68,15 @@ public class TaskIconCache implements DisplayInfoChangeListener { private final Context mContext; private final TaskKeyLruCache mIconCache; private final SparseArray mDefaultIcons = new SparseArray<>(); + private BitmapInfo mDefaultIconBase = null; + private final IconProvider mIconProvider; private BaseIconFactory mIconFactory; + @Nullable + public TaskVisualsChangeListener mTaskVisualsChangeListener = null; + public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) { mContext = context; mBgExecutor = bgExecutor; @@ -94,8 +101,9 @@ public class TaskIconCache implements DisplayInfoChangeListener { /** * Asynchronously fetches the icon and other task data. * - * @param task The task to fetch the data for - * @param callback The callback to receive the task after its data has been populated. + * @param task The task to fetch the data for + * @param callback The callback to receive the task after its data has been + * populated. * @return A cancelable handle to the request */ public CancellableTask updateIconInBackground(Task task, Consumer callback) { @@ -116,6 +124,7 @@ public class TaskIconCache implements DisplayInfoChangeListener { task.icon = result.icon; task.titleDescription = result.contentDescription; callback.accept(task); + dispatchIconUpdate(task.key.id); } }; mBgExecutor.execute(request); @@ -134,8 +143,8 @@ public class TaskIconCache implements DisplayInfoChangeListener { } void invalidateCacheEntries(String pkg, UserHandle handle) { - mBgExecutor.execute(() -> mIconCache.removeAll(key -> - pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId)); + mBgExecutor.execute(() -> mIconCache + .removeAll(key -> pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId)); } @WorkerThread @@ -194,6 +203,14 @@ public class TaskIconCache implements DisplayInfoChangeListener { return entry; } + private Bitmap getIcon(ActivityManager.TaskDescription desc, int userId) { + if (desc.getInMemoryIcon() != null) { + return desc.getInMemoryIcon(); + } + return ActivityManager.TaskDescription.loadTaskDescriptionIcon( + desc.getIconFilename(), userId); + } + private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) { PackageManager pm = mContext.getPackageManager(); String taskLabel = td == null ? null : Utilities.trim(td.getLabel()); @@ -206,33 +223,41 @@ public class TaskIconCache implements DisplayInfoChangeListener { ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString() : applicationLabel; return applicationLabel.equals(taskLabel) - ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel; + ? badgedApplicationLabel + : badgedApplicationLabel + " " + taskLabel; } @WorkerThread private Drawable getDefaultIcon(int userId) { synchronized (mDefaultIcons) { - BitmapInfo info = mDefaultIcons.get(userId); - if (info == null) { + if (mDefaultIconBase == null) { try (BaseIconFactory bif = getIconFactory()) { - info = bif.makeDefaultIcon(UserHandle.of(userId)); + mDefaultIconBase = bif.makeDefaultIcon(UserHandle.getUserHandleForUid (userId)); + } + } + + int index; + if ((index = mDefaultIcons.indexOfKey(userId)) >= 0) { + return mDefaultIcons.valueAt(index).newIcon(mContext); + } else { + try (BaseIconFactory li = getIconFactory()) { + BitmapInfo info = li.makeDefaultIcon (UserHandle.getUserHandleForUid (userId)); + mDefaultIcons.put(userId, info); + return info.newIcon(mContext); } - mDefaultIcons.put(userId, info); } - return info.newIcon(mContext); } } @WorkerThread private BitmapInfo getBitmapInfo(Drawable drawable, int userId, - int primaryColor, boolean isInstantApp) { + int primaryColor, boolean isInstantApp) { try (BaseIconFactory bif = getIconFactory()) { - bif.disableColorExtraction(); bif.setWrapperBackgroundColor(primaryColor); - // User version code O, so that the icon is always wrapped in an adaptive icon container - return bif.createBadgedIconBitmap(drawable, UserHandle.of(userId), - Build.VERSION_CODES.O, isInstantApp); + // User version code O, so that the icon is always wrapped in an adaptive icon + // container + return bif.createBadgedIconBitmap(drawable, UserHandle.getUserHandleForUid (userId), false); } } @@ -240,8 +265,9 @@ public class TaskIconCache implements DisplayInfoChangeListener { private BaseIconFactory getIconFactory() { if (mIconFactory == null) { mIconFactory = new BaseIconFactory(mContext, - DisplayController.INSTANCE.get(mContext).getInfo().densityDpi, - mContext.getResources().getDimensionPixelSize(R.dimen.taskbar_icon_size)); + DisplayController.INSTANCE.get(mContext).getInfo().getDensityDpi(), + mContext.getResources().getDimensionPixelSize( + R.dimen.task_icon_cache_default_icon_size)); } return mIconFactory; } @@ -256,4 +282,18 @@ public class TaskIconCache implements DisplayInfoChangeListener { public Drawable icon; public String contentDescription = ""; } + + void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) { + mTaskVisualsChangeListener = newListener; + } + + void removeTaskVisualsChangeListener() { + mTaskVisualsChangeListener = null; + } + + void dispatchIconUpdate(int taskId) { + if (mTaskVisualsChangeListener != null) { + mTaskVisualsChangeListener.onTaskIconChanged(taskId); + } + } } diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index ad2dc34ddc..7aa358dfd0 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -18,12 +18,10 @@ package com.android.quickstep; import static android.view.Surface.ROTATION_0; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL; import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED; import android.annotation.SuppressLint; -import android.app.ActivityManager; import android.content.Context; import android.graphics.Insets; import android.graphics.Matrix; @@ -37,16 +35,13 @@ import androidx.annotation.RequiresApi; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.ResourceBasedOverride; -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; -import com.android.quickstep.TaskShortcutFactory.SplitSelectSystemShortcut; +import com.android.launcher3.views.ActivityContext; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; @@ -55,7 +50,6 @@ import com.android.quickstep.views.TaskView; import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.ActivityManagerWrapper; import java.util.ArrayList; import java.util.List; @@ -66,7 +60,7 @@ import java.util.List; public class TaskOverlayFactory implements ResourceBasedOverride { public static List getEnabledShortcuts(TaskView taskView, - DeviceProfile deviceProfile, TaskIdAttributeContainer taskContainer) { + TaskIdAttributeContainer taskContainer) { final ArrayList shortcuts = new ArrayList<>(); final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext()); boolean hasMultipleTasks = taskView.getTaskIds()[1] != -1; @@ -75,101 +69,70 @@ public class TaskOverlayFactory implements ResourceBasedOverride { continue; } - SystemShortcut shortcut = menuOption.getShortcut(activity, taskContainer); - if (shortcut == null) { + List menuShortcuts = menuOption.getShortcuts(activity, taskContainer); + if (menuShortcuts == null) { continue; } - - if (menuOption == TaskShortcutFactory.SPLIT_SCREEN && - FeatureFlags.ENABLE_SPLIT_SELECT.get()) { - addSplitOptions(shortcuts, activity, taskView, deviceProfile); - } else { - shortcuts.add(shortcut); - } + shortcuts.addAll(menuShortcuts); } RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState(); boolean canLauncherRotate = orientedState.isRecentsActivityRotationAllowed(); boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0; + boolean isTablet = activity.getDeviceProfile().isTablet; - // Add overview actions to the menu when in in-place rotate landscape mode. - if (!canLauncherRotate && isInLandscape) { + boolean isGridOnlyOverview = isTablet && FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW.get(); + // Add overview actions to the menu when in in-place rotate landscape mode, or + // in + // grid-only overview. + if ((!canLauncherRotate && isInLandscape) || isGridOnlyOverview) { // Add screenshot action to task menu. - SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT - .getShortcut(activity, taskContainer); - if (screenshotShortcut != null) { - shortcuts.add(screenshotShortcut); + List screenshotShortcuts = TaskShortcutFactory.SCREENSHOT + .getShortcuts(activity, taskContainer); + if (screenshotShortcuts != null) { + shortcuts.addAll(screenshotShortcuts); } - // Add modal action only if display orientation is the same as the device orientation. - if (orientedState.getDisplayRotation() == ROTATION_0) { - SystemShortcut modalShortcut = TaskShortcutFactory.MODAL - .getShortcut(activity, taskContainer); - if (modalShortcut != null) { - shortcuts.add(modalShortcut); + // Add modal action only if display orientation is the same as the device + // orientation, + // or in grid-only overview. + if (orientedState.getDisplayRotation() == ROTATION_0 || isGridOnlyOverview) { + List modalShortcuts = TaskShortcutFactory.MODAL + .getShortcuts(activity, taskContainer); + if (modalShortcuts != null) { + shortcuts.addAll(modalShortcuts); } } } return shortcuts; } - - /** - * Does NOT add split options in the following scenarios: - * * The taskView to add split options is already showing split screen tasks - * * There aren't at least 2 tasks in overview to show split options for - * * Device is in "Lock task mode" - * * The taskView to show split options for is the focused task AND we haven't started - * scrolling in overview (if we haven't scrolled, there's a split overview action button so - * we don't need this menu option) - */ - private static void addSplitOptions(List outShortcuts, - BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile) { - RecentsView recentsView = taskView.getRecentsView(); - PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler(); - int[] taskViewTaskIds = taskView.getTaskIds(); - boolean taskViewHasMultipleTasks = taskViewTaskIds[0] != -1 && - taskViewTaskIds[1] != -1; - boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2; - boolean isFocusedTask = deviceProfile.overviewShowAsGrid && taskView.isFocusedTask(); - boolean isTaskInExpectedScrollPosition = - recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); - ActivityManager activityManager = - (ActivityManager) taskView.getContext().getSystemService(Context.ACTIVITY_SERVICE); - boolean isLockTaskMode = activityManager.isInLockTaskMode(); - - if (taskViewHasMultipleTasks || notEnoughTasksToSplit || isLockTaskMode || - (isFocusedTask && isTaskInExpectedScrollPosition)) { - return; - } - - List positions = - orientationHandler.getSplitPositionOptions(deviceProfile); - for (SplitPositionOption option : positions) { - outShortcuts.add(new SplitSelectSystemShortcut(activity, taskView, option)); - } - } - public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) { return new TaskOverlay(thumbnailView); } /** - * Subclasses can attach any system listeners in this method, must be paired with + * Subclasses can attach any system listeners in this method, must be paired + * with * {@link #removeListeners()} */ - public void initListeners() { } + public void initListeners() { + } /** - * Subclasses should remove any system listeners in this method, must be paired with + * Subclasses should remove any system listeners in this method, must be paired + * with * {@link #initListeners()} */ - public void removeListeners() { } + public void removeListeners() { + } - /** Note that these will be shown in order from top to bottom, if available for the task. */ - private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{ + /** + * Note that these will be shown in order from top to bottom, if available for + * the task. + */ + private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[] { TaskShortcutFactory.APP_INFO, - TaskShortcutFactory.SPLIT_SCREEN, - TaskShortcutFactory.UNINSTALL, + TaskShortcutFactory.SPLIT_SELECT, TaskShortcutFactory.PIN, TaskShortcutFactory.INSTALL, TaskShortcutFactory.FREE_FORM, @@ -191,7 +154,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { mApplicationContext = taskThumbnailView.getContext().getApplicationContext(); mThumbnailView = taskThumbnailView; mImageApi = new ImageActionsApi( - mApplicationContext, mThumbnailView::getThumbnail); + mApplicationContext, mThumbnailView::getThumbnail); } protected T getActionsView() { @@ -222,14 +185,10 @@ public class TaskOverlayFactory implements ResourceBasedOverride { * @param callback callback to run, after switching to screenshot */ public void endLiveTileMode(@NonNull Runnable callback) { - if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { - RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView(); - recentsView.switchToScreenshot( - () -> recentsView.finishRecentsAnimation(true /* toRecents */, - false /* shouldPip */, callback)); - } else { - callback.run(); - } + RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView(); + recentsView.switchToScreenshot( + () -> recentsView.finishRecentsAnimation(true /* toRecents */, + false /* shouldPip */, callback)); } /** @@ -265,7 +224,8 @@ public class TaskOverlayFactory implements ResourceBasedOverride { /** * Gets the modal state system shortcut. */ - public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) { + public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo, + View original) { return null; } @@ -276,12 +236,14 @@ public class TaskOverlayFactory implements ResourceBasedOverride { } /** - * Gets the system shortcut for the screenshot that will be added to the task menu. + * Gets the system shortcut for the screenshot that will be added to the task + * menu. */ public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity, - ItemInfo iteminfo) { - return new ScreenshotSystemShortcut(activity, iteminfo); + ItemInfo iteminfo, View originalView) { + return new ScreenshotSystemShortcut(activity, iteminfo, originalView); } + /** * Gets the task snapshot as it is displayed on the screen. * @@ -312,18 +274,29 @@ public class TaskOverlayFactory implements ResourceBasedOverride { } protected void showBlockedByPolicyMessage() { + ActivityContext activityContext = ActivityContext.lookupContext( + mThumbnailView.getContext()); + String message = activityContext.getStringCache() != null + ? activityContext.getStringCache().disabledByAdminMessage + : mThumbnailView.getContext().getString(R.string.blocked_by_policy); Toast.makeText( mThumbnailView.getContext(), - R.string.blocked_by_policy, + message, Toast.LENGTH_LONG).show(); } + /** Called when the snapshot has updated its full screen drawing parameters. */ + public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { + } + private class ScreenshotSystemShortcut extends SystemShortcut { private final BaseDraggingActivity mActivity; - ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) { - super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo); + ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo, + View originalView) { + super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo, + originalView); mActivity = activity; } @@ -355,7 +328,8 @@ public class TaskOverlayFactory implements ResourceBasedOverride { } /** - * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the + * Callbacks the Ui can generate. This is the only way for a Ui to call methods + * on the * controller. */ public interface OverlayUICallbacks { diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java index 3427c719ce..24f9e8a86f 100644 --- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java +++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java @@ -16,11 +16,9 @@ package com.android.quickstep; -import static android.view.Display.DEFAULT_DISPLAY; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP; import android.app.Activity; import android.app.ActivityOptions; @@ -29,9 +27,16 @@ import android.graphics.Color; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.util.Log; import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManagerGlobal; import android.window.SplashScreen; +import androidx.annotation.Nullable; + import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; @@ -40,6 +45,7 @@ import com.android.launcher3.logging.StatsLogManager.LauncherEvent; import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.popup.SystemShortcut.AppInfo; +import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.util.PackageManagerHelper; @@ -51,36 +57,49 @@ import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; import com.android.systemui.shared.recents.view.RecentsTransition; -import com.android.systemui.shared.system.ActivityCompat; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.ActivityOptionsCompat; -import com.android.systemui.shared.system.WindowManagerWrapper; import java.util.Collections; import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; /** * Represents a system shortcut that can be shown for a recent task. */ public interface TaskShortcutFactory { - SystemShortcut getShortcut(BaseDraggingActivity activity, - TaskIdAttributeContainer taskContainer); + @Nullable + default List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + return null; + } default boolean showForSplitscreen() { return false; } + /** + * @return a singleton list if the provided shortcut is non-null, null otherwise + */ + @Nullable + default List createSingletonShortcutList(@Nullable SystemShortcut shortcut) { + if (shortcut != null) { + return Collections.singletonList(shortcut); + } + return null; + } + TaskShortcutFactory APP_INFO = new TaskShortcutFactory() { @Override - public SystemShortcut getShortcut(BaseDraggingActivity activity, + public List getShortcuts(BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer) { TaskView taskView = taskContainer.getTaskView(); - AppInfo.SplitAccessibilityInfo accessibilityInfo = - new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(), - TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()), - taskContainer.getA11yNodeId() - ); - return new AppInfo(activity, taskContainer.getItemInfo(), accessibilityInfo); + AppInfo.SplitAccessibilityInfo accessibilityInfo = new AppInfo.SplitAccessibilityInfo( + taskView.containsMultipleTasks(), + TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()), + taskContainer.getA11yNodeId()); + return Collections.singletonList(new AppInfo(activity, taskContainer.getItemInfo(), + taskView, accessibilityInfo)); } @Override @@ -89,48 +108,13 @@ public interface TaskShortcutFactory { } }; - TaskShortcutFactory UNINSTALL = (activity, view) -> - PackageManagerHelper.isSystemApp(activity, - view.getTask().getTopComponent().getPackageName()) - ? null : new SystemShortcut.UnInstall(activity, view.getItemInfo()); - - abstract class MultiWindowFactory implements TaskShortcutFactory { - - private final int mIconRes; - private final int mTextRes; - private final LauncherEvent mLauncherEvent; - - MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) { - mIconRes = iconRes; - mTextRes = textRes; - mLauncherEvent = launcherEvent; - } - - protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId); - protected abstract ActivityOptions makeLaunchOptions(Activity activity); - protected abstract boolean onActivityStarted(BaseDraggingActivity activity); - - @Override - public SystemShortcut getShortcut(BaseDraggingActivity activity, - TaskIdAttributeContainer taskContainer) { - final Task task = taskContainer.getTask(); - if (!task.isDockable) { - return null; - } - if (!isAvailable(activity, task.key.displayId)) { - return null; - } - return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskContainer, this, - mLauncherEvent); - } - } - class SplitSelectSystemShortcut extends SystemShortcut { private final TaskView mTaskView; private final SplitPositionOption mSplitPositionOption; + public SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView, SplitPositionOption option) { - super(option.iconResId, option.textResId, target, taskView.getItemInfo()); + super(option.iconResId, option.textResId, target, taskView.getItemInfo(), taskView); mTaskView = taskView; mSplitPositionOption = option; } @@ -141,78 +125,53 @@ public interface TaskShortcutFactory { } } - class MultiWindowSystemShortcut extends SystemShortcut { + class FreeformSystemShortcut extends SystemShortcut { + private static final String TAG = "FreeformSystemShortcut"; private Handler mHandler; private final RecentsView mRecentsView; private final TaskThumbnailView mThumbnailView; private final TaskView mTaskView; - private final MultiWindowFactory mFactory; private final LauncherEvent mLauncherEvent; - public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, - TaskIdAttributeContainer taskContainer, MultiWindowFactory factory, - LauncherEvent launcherEvent) { - super(iconRes, textRes, activity, taskContainer.getItemInfo()); + public FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) { + super(iconRes, textRes, activity, taskContainer.getItemInfo(), + taskContainer.getTaskView()); mLauncherEvent = launcherEvent; mHandler = new Handler(Looper.getMainLooper()); mTaskView = taskContainer.getTaskView(); mRecentsView = activity.getOverviewPanel(); mThumbnailView = taskContainer.getThumbnailView(); - mFactory = factory; } @Override public void onClick(View view) { - Task.TaskKey taskKey = mTaskView.getTask().key; - final int taskId = taskKey.id; - - final View.OnLayoutChangeListener onLayoutChangeListener = - new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int l, int t, int r, int b, - int oldL, int oldT, int oldR, int oldB) { - mTaskView.getRootView().removeOnLayoutChangeListener(this); - mRecentsView.clearIgnoreResetTask(taskId); - - // Start animating in the side pages once launcher has been resized - mRecentsView.dismissTask(mTaskView, false, false); - } - }; - - final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = - new DeviceProfile.OnDeviceProfileChangeListener() { - @Override - public void onDeviceProfileChanged(DeviceProfile dp) { - mTarget.removeOnDeviceProfileChangeListener(this); - if (dp.isMultiWindowMode) { - mTaskView.getRootView().addOnLayoutChangeListener( - onLayoutChangeListener); - } - } - }; - dismissTaskMenuView(mTarget); + RecentsView rv = mTarget.getOverviewPanel(); + rv.switchToScreenshot(() -> { + rv.finishRecentsAnimation(true /* toHome */, () -> { + mTarget.returnToHomescreen(); + rv.getHandler().post(this::startActivity); + }); + }); + } - ActivityOptions options = mFactory.makeLaunchOptions(mTarget); - if (options != null && Utilities.ATLEAST_S) { - options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); + private void startActivity() { + final Task.TaskKey taskKey = mTaskView.getTask().key; + final int taskId = taskKey.id; + final ActivityOptions options = makeLaunchOptions(mTarget); + if (options != null) { + options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); } if (options != null && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, options)) { - if (!mFactory.onActivityStarted(mTarget)) { - return; - } - // Add a device profile change listener to kick off animating the side tasks - // once we enter multiwindow mode and relayout - mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener); - final Runnable animStartedListener = () -> { // Hide the task view and wait for the window to be resized // TODO: Consider animating in launcher and do an in-place start activity - // afterwards + // afterwards mRecentsView.setIgnoreResetTask(taskId); mTaskView.setAlpha(0f); }; @@ -232,91 +191,143 @@ public interface TaskShortcutFactory { Color.BLACK); mThumbnailView.setDimAlpha(alpha); - AppTransitionAnimationSpecsFuture future = - new AppTransitionAnimationSpecsFuture(mHandler) { + AppTransitionAnimationSpecsFuture future = new AppTransitionAnimationSpecsFuture(mHandler) { @Override public List composeSpecs() { return Collections.singletonList(new AppTransitionAnimationSpecCompat( taskId, thumbnail, taskBounds)); } }; - WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( + overridePendingAppTransitionMultiThumbFuture( future, animStartedListener, mHandler, true /* scaleUp */, taskKey.displayId); mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo()) .log(mLauncherEvent); } } - } - /** @Deprecated */ - TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen, - R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) { - - @Override - protected boolean isAvailable(BaseDraggingActivity activity, int displayId) { - // Don't show menu-item if already in multi-window and the task is from - // the secondary display. - // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new - // implementation is enabled - return !activity.getDeviceProfile().isMultiWindowMode - && (displayId == -1 || displayId == DEFAULT_DISPLAY); - } - - @Override - protected ActivityOptions makeLaunchOptions(Activity activity) { - final ActivityCompat act = new ActivityCompat(activity); - final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition( - act.getDisplayId()); - if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { - return null; + /** + * Overrides a pending app transition. + */ + private void overridePendingAppTransitionMultiThumbFuture( + AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, + Handler animStartedCallbackHandler, boolean scaleUp, int displayId) { + try { + WindowManagerGlobal.getWindowManagerService() + .overridePendingAppTransitionMultiThumbFuture( + animationSpecFuture.getFuture(), + RecentsTransition.wrapStartedListener(animStartedCallbackHandler, + animStartedCallback), + scaleUp, displayId); + } catch (RemoteException e) { + Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ", + e); } - boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; - return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft); } - @Override - protected boolean onActivityStarted(BaseDraggingActivity activity) { - return true; - } - }; - - TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen, - R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) { - - @Override - protected boolean isAvailable(BaseDraggingActivity activity, int displayId) { - return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity); - } - - @Override - protected ActivityOptions makeLaunchOptions(Activity activity) { - ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions(); + private ActivityOptions makeLaunchOptions(Activity activity) { + ActivityOptions activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); // Arbitrary bounds only because freeform is in dev mode right now - Rect r = new Rect(50, 50, 200, 200); + final View decorView = activity.getWindow().getDecorView(); + final WindowInsets insets = decorView.getRootWindowInsets(); + final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2); + r.offsetTo(insets.getSystemWindowInsetLeft() + 50, + insets.getSystemWindowInsetTop() + 50); activityOptions.setLaunchBounds(r); return activityOptions; } + } + /** + * Does NOT add split options in the following scenarios: + * * The taskView to add split options is already showing split screen tasks + * * There aren't at least 2 tasks in overview to show split options for + * * Split isn't supported by the task itself (non resizable activity) + * * We aren't currently in multi-window + * * The taskView to show split options for is the focused task AND we haven't + * started + * scrolling in overview (if we haven't scrolled, there's a split overview + * action button so + * we don't need this menu option) + */ + TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() { @Override - protected boolean onActivityStarted(BaseDraggingActivity activity) { - activity.returnToHomescreen(); - return true; + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + DeviceProfile deviceProfile = activity.getDeviceProfile(); + final Task task = taskContainer.getTask(); + final TaskView taskView = taskContainer.getTaskView(); + final RecentsView recentsView = taskView.getRecentsView(); + final PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler(); + + int[] taskViewTaskIds = taskView.getTaskIds(); + boolean taskViewHasMultipleTasks = taskViewTaskIds[0] != -1 && + taskViewTaskIds[1] != -1; + boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2; + boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask(); + boolean isTaskInExpectedScrollPosition = recentsView + .isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); + boolean isTaskSplitNotSupported = !task.isDockable; + boolean hideForExistingMultiWindow = activity.getDeviceProfile().isMultiWindowMode; + + if (taskViewHasMultipleTasks || + notEnoughTasksToSplit || + isTaskSplitNotSupported || + hideForExistingMultiWindow || + (isFocusedTask && isTaskInExpectedScrollPosition)) { + return null; + } + + return orientationHandler.getSplitPositionOptions(deviceProfile) + .stream() + .map((Function) option -> new SplitSelectSystemShortcut( + activity, taskView, option)) + .collect(Collectors.toList()); } }; - TaskShortcutFactory PIN = (activity, taskContainer) -> { - if (!SystemUiProxy.INSTANCE.get(activity).isActive()) { - return null; + TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + final Task task = taskContainer.getTask(); + if (!task.isDockable) { + return null; + } + if (!isAvailable(activity, task.key.displayId)) { + return null; + } + + return Collections.singletonList(new FreeformSystemShortcut( + R.drawable.ic_caption_desktop_button_foreground, + R.string.recent_task_option_freeform, activity, taskContainer, + LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP)); } - if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { - return null; + + private boolean isAvailable(BaseDraggingActivity activity, int displayId) { + return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity) + && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false) + && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); } - if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { - // We shouldn't be able to pin while an app is locked. - return null; + }; + + TaskShortcutFactory PIN = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + if (!SystemUiProxy.INSTANCE.get(activity).isActive()) { + return null; + } + if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { + return null; + } + if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { + // We shouldn't be able to pin while an app is locked. + return null; + } + return Collections.singletonList(new PinSystemShortcut(activity, taskContainer)); } - return new PinSystemShortcut(activity, taskContainer); }; class PinSystemShortcut extends SystemShortcut { @@ -328,7 +339,7 @@ public interface TaskShortcutFactory { public PinSystemShortcut(BaseDraggingActivity target, TaskIdAttributeContainer taskContainer) { super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, - taskContainer.getItemInfo()); + taskContainer.getItemInfo(), taskContainer.getTaskView()); mTaskView = taskContainer.getTaskView(); } @@ -343,23 +354,48 @@ public interface TaskShortcutFactory { } } - TaskShortcutFactory INSTALL = (activity, taskContainer) -> - InstantAppResolver.newInstance(activity).isInstantApp(activity, - taskContainer.getTask().getTopComponent().getPackageName()) - ? new SystemShortcut.Install(activity, taskContainer.getItemInfo()) : null; - - TaskShortcutFactory WELLBEING = (activity, taskContainer) -> - WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, taskContainer.getItemInfo()); - - TaskShortcutFactory SCREENSHOT = (activity, taskContainer) -> - taskContainer.getThumbnailView().getTaskOverlay() - .getScreenshotShortcut(activity, taskContainer.getItemInfo()); - - TaskShortcutFactory MODAL = (activity, taskContainer) -> { - if (ENABLE_OVERVIEW_SELECTIONS.get()) { - return taskContainer.getThumbnailView() - .getTaskOverlay().getModalStateSystemShortcut(taskContainer.getItemInfo()); + TaskShortcutFactory INSTALL = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + return InstantAppResolver.newInstance(activity).isInstantApp(activity, + taskContainer.getTask().getTopComponent().getPackageName()) + ? Collections.singletonList(new SystemShortcut.Install(activity, + taskContainer.getItemInfo(), taskContainer.getTaskView())) + : null; + } + }; + + TaskShortcutFactory WELLBEING = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + SystemShortcut wellbeingShortcut = WellbeingModel.SHORTCUT_FACTORY.getShortcut( + activity, + taskContainer.getItemInfo(), taskContainer.getTaskView()); + return createSingletonShortcutList(wellbeingShortcut); + } + }; + + TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + SystemShortcut screenshotShortcut = taskContainer.getThumbnailView().getTaskOverlay() + .getScreenshotShortcut(activity, taskContainer.getItemInfo(), + taskContainer.getTaskView()); + return createSingletonShortcutList(screenshotShortcut); + } + }; + + TaskShortcutFactory MODAL = new TaskShortcutFactory() { + @Override + public List getShortcuts(BaseDraggingActivity activity, + TaskIdAttributeContainer taskContainer) { + SystemShortcut modalStateSystemShortcut = taskContainer.getThumbnailView().getTaskOverlay() + .getModalStateSystemShortcut( + taskContainer.getItemInfo(), taskContainer.getTaskView()); + return createSingletonShortcutList(modalStateSystemShortcut); } - return null; }; } diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java index 19f3186430..9d34e8c4eb 100644 --- a/quickstep/src/com/android/quickstep/TaskUtils.java +++ b/quickstep/src/com/android/quickstep/TaskUtils.java @@ -18,19 +18,23 @@ package com.android.quickstep; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.UserHandle; import android.util.Log; +import android.view.RemoteAnimationTarget; + +import androidx.annotation.Nullable; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import java.util.List; @@ -49,16 +53,32 @@ public final class TaskUtils { * TODO: remove this once we switch to getting the icon and label from IconCache. */ public static CharSequence getTitle(Context context, Task task) { - UserHandle user = UserHandle.of(task.key.userId); + return getTitle(context, task.key.userId, task.getTopComponent().getPackageName()); + } + + public static CharSequence getTitle( + @NonNull Context context, + @UserIdInt @Nullable Integer userId, + @Nullable String packageName) { + if (userId == null || packageName == null) { + if (userId == null) { + Log.e(TAG, "Failed to get title; missing userId"); + } + if (packageName == null) { + Log.e(TAG, "Failed to get title; missing packageName"); + } + return ""; + } + UserHandle user = UserHandle.of(userId); ApplicationInfo applicationInfo = new PackageManagerHelper(context) - .getApplicationInfo(task.getTopComponent().getPackageName(), user, 0); + .getApplicationInfo(packageName, user, 0); if (applicationInfo == null) { - Log.e(TAG, "Failed to get title for task " + task); + Log.e(TAG, "Failed to get title for userId=" + userId + ", packageName=" + packageName); return ""; } PackageManager packageManager = context.getPackageManager(); return packageManager.getUserBadgedLabel( - applicationInfo.loadLabel(packageManager), user); + applicationInfo.loadLabel(packageManager), user); } public static ComponentKey getLaunchComponentKeyForTask(Task.TaskKey taskKey) { @@ -69,9 +89,9 @@ public final class TaskUtils { } - public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets, + public static boolean taskIsATargetWithMode(RemoteAnimationTarget[] targets, int taskId, int mode) { - for (RemoteAnimationTargetCompat target : targets) { + for (RemoteAnimationTarget target : targets) { if (target.mode == mode && target.taskId == taskId) { return true; } diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index dbf8acf39b..f0d0bdb0ed 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -15,11 +15,15 @@ */ package com.android.quickstep; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; +import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_DELAY_NAV_FADE_IN; @@ -34,17 +38,12 @@ import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncest import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; import static com.android.launcher3.anim.Interpolators.clampToProgress; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; -import static com.android.launcher3.statehandlers.DepthController.DEPTH; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; +import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.ComponentName; @@ -54,7 +53,7 @@ import android.graphics.Matrix.ScaleToFit; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; -import android.util.Log; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.View; import android.window.TransitionInfo; @@ -64,6 +63,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; @@ -74,9 +74,12 @@ import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.util.DisplayController; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.SurfaceTransaction; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskThumbnailView; @@ -84,7 +87,6 @@ import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; import java.util.ArrayList; import java.util.List; @@ -106,7 +108,7 @@ public final class TaskViewUtils { * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView. */ public static TaskView findTaskViewToLaunch( - RecentsView recentsView, View v, RemoteAnimationTargetCompat[] targets) { + RecentsView recentsView, View v, RemoteAnimationTarget[] targets) { if (v instanceof TaskView) { TaskView taskView = (TaskView) v; return recentsView.isTaskViewVisible(taskView) ? taskView : null; @@ -136,7 +138,7 @@ public final class TaskViewUtils { } // Resolve the opening task id int openingTaskId = -1; - for (RemoteAnimationTargetCompat target : targets) { + for (RemoteAnimationTarget target : targets) { if (target.mode == MODE_OPENING) { openingTaskId = target.taskId; break; @@ -159,21 +161,19 @@ public final class TaskViewUtils { public static void createRecentsWindowAnimator( @NonNull TaskView v, boolean skipViewChanges, - @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets, + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, @Nullable DepthController depthController, PendingAnimation out) { RecentsView recentsView = v.getRecentsView(); boolean isQuickSwitch = v.isEndQuickswitchCuj(); v.setEndQuickswitchCuj(false); - boolean inLiveTileMode = - ENABLE_QUICKSTEP_LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1; final RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets, - inLiveTileMode ? MODE_CLOSING : MODE_OPENING); - final RemoteAnimationTargetCompat navBarTarget = targets.getNavBarRemoteAnimationTarget(); + MODE_OPENING); + final RemoteAnimationTarget navBarTarget = targets.getNavBarRemoteAnimationTarget(); SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v); targets.addReleaseCheck(applier); @@ -184,10 +184,13 @@ public final class TaskViewUtils { // Re-use existing handles remoteTargetHandles = recentsViewHandles; } else { + boolean forDesktop = DESKTOP_MODE_SUPPORTED && v instanceof DesktopTaskView; RemoteTargetGluer gluer = new RemoteTargetGluer(v.getContext(), - recentsView.getSizeStrategy(), targets); - if (v.containsMultipleTasks()) { - remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets, v.getTaskIds()); + recentsView.getSizeStrategy(), targets, forDesktop); + if (forDesktop) { + remoteTargetHandles = gluer.assignTargetsForDesktop(targets); + } else if (v.containsMultipleTasks()) { + remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets); } else { remoteTargetHandles = gluer.assignTargets(targets); } @@ -198,10 +201,10 @@ public final class TaskViewUtils { int taskIndex = recentsView.indexOfChild(v); Context context = v.getContext(); - DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile(); - boolean showAsGrid = dp.overviewShowAsGrid; - boolean parallaxCenterAndAdjacentTask = - taskIndex != recentsView.getCurrentPage() && !showAsGrid; + BaseActivity baseActivity = BaseActivity.fromContext(context); + DeviceProfile dp = baseActivity.getDeviceProfile(); + boolean showAsGrid = dp.isTablet; + boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage(); int taskRectTranslationPrimary = recentsView.getScrollOffset(taskIndex); int taskRectTranslationSecondary = showAsGrid ? (int) v.getGridTranslationY() : 0; @@ -256,25 +259,28 @@ public final class TaskViewUtils { @Override public void onUpdate(float percent, boolean initOnly) { - final SurfaceParams.Builder navBuilder = - new SurfaceParams.Builder(navBarTarget.leash); + // TODO Do we need to operate over multiple TVSs for the navbar leash? for (RemoteTargetHandle handle : remoteTargetHandles) { + SurfaceTransaction transaction = new SurfaceTransaction(); + SurfaceProperties navBuilder = + transaction.forSurface(navBarTarget.leash); + if (mNavFadeIn.value > mNavFadeIn.getStartValue()) { TaskViewSimulator taskViewSimulator = handle.getTaskViewSimulator(); taskViewSimulator.getCurrentCropRect().round(cropRect); - navBuilder.withMatrix(taskViewSimulator.getCurrentMatrix()) - .withWindowCrop(cropRect) - .withAlpha(mNavFadeIn.value); + navBuilder.setMatrix(taskViewSimulator.getCurrentMatrix()) + .setWindowCrop(cropRect) + .setAlpha(mNavFadeIn.value); } else { - navBuilder.withAlpha(mNavFadeOut.value); + navBuilder.setAlpha(mNavFadeOut.value); } - handle.getTransformParams().applySurfaceParams(navBuilder.build()); + handle.getTransformParams().applySurfaceParams(transaction); } } }); - } else if (inLiveTileMode) { + } else { // There is no transition animation for app launch from recent in live tile mode so // we have to trigger the navigation bar animation from system here. final RecentsAnimationController controller = @@ -286,7 +292,8 @@ public final class TaskViewUtils { topMostSimulators = remoteTargetHandles; } - if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulators.length > 0) { + if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulators != null + && topMostSimulators.length > 0) { out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f)); RemoteTargetHandle[] simulatorCopies = topMostSimulators; @@ -307,9 +314,15 @@ public final class TaskViewUtils { // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is: // Mt K(0)` K(t) Mt` TaskThumbnailView[] thumbnails = v.getThumbnails(); - Matrix[] mt = new Matrix[simulatorCopies.length]; - Matrix[] mti = new Matrix[simulatorCopies.length]; - for (int i = 0; i < thumbnails.length; i++) { + + // In case simulator copies and thumbnail size do no match, ensure we get the lesser. + // This ensures we do not create arrays with empty elements or attempt to references + // indexes out of array bounds. + final int matrixSize = Math.min(simulatorCopies.length, thumbnails.length); + + Matrix[] mt = new Matrix[matrixSize]; + Matrix[] mti = new Matrix[matrixSize]; + for (int i = 0; i < matrixSize; i++) { TaskThumbnailView ttv = thumbnails[i]; RectF localBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight()); float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()}; @@ -324,16 +337,27 @@ public final class TaskViewUtils { Matrix localMti = new Matrix(); localMt.invert(localMti); mti[i] = localMti; + + // Translations for child thumbnails also get scaled as the parent taskView scales + // Add inverse scaling to keep translations the same + float translationY = ttv.getTranslationY(); + float translationX = ttv.getTranslationX(); + float fullScreenScale = + topMostSimulators[i].getTaskViewSimulator().getFullScreenScale(); + out.addFloat(ttv, VIEW_TRANSLATE_Y, translationY, + translationY / fullScreenScale, TOUCH_RESPONSE_INTERPOLATOR); + out.addFloat(ttv, VIEW_TRANSLATE_X, translationX, + translationX / fullScreenScale, TOUCH_RESPONSE_INTERPOLATOR); } - Matrix[] k0i = new Matrix[simulatorCopies.length]; - for (int i = 0; i < simulatorCopies.length; i++) { + Matrix[] k0i = new Matrix[matrixSize]; + for (int i = 0; i < matrixSize; i++) { k0i[i] = new Matrix(); simulatorCopies[i].getTaskViewSimulator().getCurrentMatrix().invert(k0i[i]); } Matrix animationMatrix = new Matrix(); out.addOnFrameCallback(() -> { - for (int i = 0; i < simulatorCopies.length; i++) { + for (int i = 0; i < matrixSize; i++) { animationMatrix.set(mt[i]); animationMatrix.postConcat(k0i[i]); animationMatrix.postConcat(simulatorCopies[i] @@ -370,8 +394,8 @@ public final class TaskViewUtils { }); if (depthController != null) { - out.setFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(context), - TOUCH_RESPONSE_INTERPOLATOR); + out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE, + BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE_INTERPOLATOR); } } @@ -391,18 +415,50 @@ public final class TaskViewUtils { * device is considered in multiWindowMode and things like insets and stuff change * and calculations have to be adjusted in the animations for that */ - public static void composeRecentsSplitLaunchAnimator(@NonNull Task initalTask, - @NonNull Task secondTask, @NonNull TransitionInfo transitionInfo, + public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView, + @NonNull StateManager stateManager, @Nullable DepthController depthController, + int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { + if (launchingTaskView != null) { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishCallback.run(); + } + }); - final TransitionInfo.Change[] splitRoots = new TransitionInfo.Change[2]; + final RemoteAnimationTarget[] appTargets = + RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */); + final RemoteAnimationTarget[] wallpaperTargets = + RemoteAnimationTargetCompat.wrapNonApps( + transitionInfo, true /* wallpapers */, t, null /* leashMap */); + final RemoteAnimationTarget[] nonAppTargets = + RemoteAnimationTargetCompat.wrapNonApps( + transitionInfo, false /* wallpapers */, t, null /* leashMap */); + final RecentsView recentsView = launchingTaskView.getRecentsView(); + composeRecentsLaunchAnimator(animatorSet, launchingTaskView, + appTargets, wallpaperTargets, nonAppTargets, + true, stateManager, + recentsView, depthController); + + t.apply(); + animatorSet.start(); + return; + } + + // TODO: consider initialTaskPendingIntent + TransitionInfo.Change splitRoot1 = null; + TransitionInfo.Change splitRoot2 = null; for (int i = 0; i < transitionInfo.getChanges().size(); ++i) { final TransitionInfo.Change change = transitionInfo.getChanges().get(i); - final int taskId = change.getTaskInfo() != null ? change.getTaskInfo().taskId : -1; + if (change.getTaskInfo() == null) continue; + final int taskId = change.getTaskInfo().taskId; final int mode = change.getMode(); + // Find the target tasks' root tasks since those are the split stages that need to // be animated (the tasks themselves are children and thus inherit animation). - if (taskId == initalTask.key.id || taskId == secondTask.key.id) { + if (taskId == initialTaskId || taskId == secondTaskId) { if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { throw new IllegalStateException( "Expected task to be showing, but it is " + mode); @@ -411,16 +467,18 @@ public final class TaskViewUtils { throw new IllegalStateException("Initiating multi-split launch but the split" + "root of " + taskId + " is already visible or has broken hierarchy."); } - splitRoots[taskId == initalTask.key.id ? 0 : 1] = - transitionInfo.getChange(change.getParent()); + } + if (taskId == initialTaskId) { + splitRoot1 = transitionInfo.getChange(change.getParent()); + } + if (taskId == secondTaskId) { + splitRoot2 = transitionInfo.getChange(change.getParent()); } } // This is where we should animate the split roots. For now, though, just make them visible. - for (int i = 0; i < 2; ++i) { - t.show(splitRoots[i].getLeash()); - t.setAlpha(splitRoots[i].getLeash(), 1.f); - } + animateSplitRoot(t, splitRoot1); + animateSplitRoot(t, splitRoot2); // This contains the initial state (before animation), so apply this at the beginning of // the animation. @@ -430,23 +488,30 @@ public final class TaskViewUtils { finishCallback.run(); } + private static void animateSplitRoot(SurfaceControl.Transaction t, + TransitionInfo.Change splitRoot) { + if (splitRoot != null) { + t.show(splitRoot.getLeash()); + t.setAlpha(splitRoot.getLeash(), 1.f); + } + } + /** * Legacy version (until shell transitions are enabled) * * If {@param launchingTaskView} is not null, then this will play the tasks launch animation * from the position of the GroupedTaskView (when user taps on the TaskView to start it). * Technically this case should be taken care of by - * {@link #composeRecentsSplitLaunchAnimatorLegacy()} below, but the way we launch tasks whether + * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether * it's a single task or multiple tasks results in different entry-points. * * If it is null, then it will simply fade in the starting apps and fade out launcher (for the * case where launcher handles animating starting split tasks from app icon) */ public static void composeRecentsSplitLaunchAnimatorLegacy( - @Nullable GroupedTaskView launchingTaskView, - @NonNull Task initialTask, - @NonNull Task secondTask, @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets, + @Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId, + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, @NonNull StateManager stateManager, @Nullable DepthController depthController, @NonNull Runnable finishCallback) { @@ -456,7 +521,6 @@ public final class TaskViewUtils { animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); finishCallback.run(); } }); @@ -470,17 +534,17 @@ public final class TaskViewUtils { final ArrayList openingTargets = new ArrayList<>(); final ArrayList closingTargets = new ArrayList<>(); - for (RemoteAnimationTargetCompat appTarget : appTargets) { + for (RemoteAnimationTarget appTarget : appTargets) { final int taskId = appTarget.taskInfo != null ? appTarget.taskInfo.taskId : -1; final int mode = appTarget.mode; - final SurfaceControl leash = appTarget.leash.getSurfaceControl(); + final SurfaceControl leash = appTarget.leash; if (leash == null) { continue; } if (mode == MODE_OPENING) { openingTargets.add(leash); - } else if (taskId == initialTask.key.id || taskId == secondTask.key.id) { + } else if (taskId == initialTaskId || taskId == secondTaskId) { throw new IllegalStateException("Expected task to be opening, but it is " + mode); } else if (mode == MODE_CLOSING) { closingTargets.add(leash); @@ -488,7 +552,7 @@ public final class TaskViewUtils { } for (int i = 0; i < nonAppTargets.length; ++i) { - final SurfaceControl leash = appTargets[i].leash.getSurfaceControl(); + final SurfaceControl leash = nonAppTargets[i].leash; if (nonAppTargets[i].windowType == TYPE_DOCK_DIVIDER && leash != null) { openingTargets.add(leash); } @@ -502,9 +566,6 @@ public final class TaskViewUtils { for (SurfaceControl leash: openingTargets) { t.setAlpha(leash, progress); } - for (SurfaceControl leash: closingTargets) { - t.setAlpha(leash, 1 - progress); - } t.apply(); }); animator.addListener(new AnimatorListenerAdapter() { @@ -521,7 +582,6 @@ public final class TaskViewUtils { for (SurfaceControl leash: closingTargets) { t.hide(leash); } - super.onAnimationEnd(animation); finishCallback.run(); } }); @@ -529,9 +589,9 @@ public final class TaskViewUtils { } public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, - @NonNull RemoteAnimationTargetCompat[] appTargets, - @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, - @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing, + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing, @NonNull StateManager stateManager, @NonNull RecentsView recentsView, @Nullable DepthController depthController) { boolean skipLauncherChanges = !launcherClosing; @@ -559,39 +619,26 @@ public final class TaskViewUtils { Animator launcherAnim; final AnimatorListenerAdapter windowAnimEndListener; if (launcherClosing) { - Context context = v.getContext(); - DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile(); - launcherAnim = dp.overviewShowAsGrid - ? ObjectAnimator.ofFloat(recentsView, RecentsView.CONTENT_ALPHA, 0) - : recentsView.createAdjacentPageAnimForTaskLaunch(taskView); - if (dp.isTablet) { - Log.d(BAD_STATE, "TVU composeRecentsLaunchAnimator alpha=" + 0); - launcherAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - Log.d(BAD_STATE, "TVU composeRecentsLaunchAnimator onStart"); - } - - @Override - public void onAnimationCancel(Animator animation) { - float alpha = recentsView == null - ? -1 - : RecentsView.CONTENT_ALPHA.get(recentsView); - Log.d(BAD_STATE, "TVU composeRecentsLaunchAnimator onCancel, alpha=" - + alpha); - } - - @Override - public void onAnimationEnd(Animator animation) { - Log.d(BAD_STATE, "TVU composeRecentsLaunchAnimator onEnd"); - } - }); + // Since Overview is in launcher, just opening overview sets willFinishToHome to true. + // Now that we are closing the launcher, we need to (re)set willFinishToHome back to + // false. Otherwise, RecentsAnimationController can't differentiate between closing + // overview to 3p home vs closing overview to app. + final RecentsAnimationController raController = + recentsView.getRecentsAnimationController(); + if (raController != null) { + raController.setWillFinishToHome(false); } + launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView); launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); launcherAnim.setDuration(RECENTS_LAUNCH_DURATION); - // Make sure recents gets fixed up by resetting task alphas and scales, etc. windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + recentsView.onTaskLaunchedInLiveTileMode(); + } + + // Make sure recents gets fixed up by resetting task alphas and scales, etc. @Override public void onAnimationEnd(Animator animation) { recentsView.finishRecentsAnimation(false /* toRecents */, () -> { @@ -617,7 +664,7 @@ public final class TaskViewUtils { }; } pa.add(launcherAnim); - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) { + if (recentsView.getRunningTaskIndex() != -1) { pa.addOnFrameCallback(recentsView::redrawLiveTile); } anim.play(pa.buildAnim()); @@ -632,28 +679,36 @@ public final class TaskViewUtils { /** * Creates an animation to show/hide the auxiliary surfaces (aka. divider bar), only calling * {@param animatorHandler} if there are valid surfaces to animate. + * Passing null handler to apply the visibility immediately. * * @return the animator animating the surfaces */ public static ValueAnimator createSplitAuxiliarySurfacesAnimator( - RemoteAnimationTargetCompat[] nonApps, boolean shown, - Consumer animatorHandler) { + @Nullable RemoteAnimationTarget[] nonApps, boolean shown, + @Nullable Consumer animatorHandler) { if (nonApps == null || nonApps.length == 0) { return null; } - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - List auxiliarySurfaces = new ArrayList<>(nonApps.length); - boolean hasSurfaceToAnimate = false; - for (int i = 0; i < nonApps.length; ++i) { - final RemoteAnimationTargetCompat targ = nonApps[i]; - final SurfaceControl leash = targ.leash.getSurfaceControl(); - if (targ.windowType == TYPE_DOCK_DIVIDER && leash != null) { + List auxiliarySurfaces = new ArrayList<>(); + for (RemoteAnimationTarget target : nonApps) { + final SurfaceControl leash = target.leash; + if (target.windowType == TYPE_DOCK_DIVIDER && leash != null && leash.isValid()) { auxiliarySurfaces.add(leash); - hasSurfaceToAnimate = true; } } - if (!hasSurfaceToAnimate) { + if (auxiliarySurfaces.isEmpty()) { + return null; + } + + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + if (animatorHandler == null) { + // Apply the visibility directly without fade animation. + for (SurfaceControl leash : auxiliarySurfaces) { + t.setVisibility(leash, shown); + } + t.apply(); + t.close(); return null; } @@ -661,16 +716,18 @@ public final class TaskViewUtils { dockFadeAnimator.addUpdateListener(valueAnimator -> { float progress = valueAnimator.getAnimatedFraction(); for (SurfaceControl leash : auxiliarySurfaces) { - t.setAlpha(leash, shown ? progress : 1 - progress); + if (leash != null && leash.isValid()) { + t.setAlpha(leash, shown ? progress : 1 - progress); + } } t.apply(); }); dockFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); if (shown) { for (SurfaceControl leash : auxiliarySurfaces) { + t.setLayer(leash, Integer.MAX_VALUE); t.setAlpha(leash, 0); t.show(leash); } @@ -680,10 +737,11 @@ public final class TaskViewUtils { @Override public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); if (!shown) { for (SurfaceControl leash : auxiliarySurfaces) { - t.hide(leash); + if (leash != null && leash.isValid()) { + t.hide(leash); + } } t.apply(); } diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java new file mode 100644 index 0000000000..6f502d0ece --- /dev/null +++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.content.Intent.ACTION_CHOOSER; +import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; + +import android.annotation.UserIdInt; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitStageInfo; +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; +import com.android.launcher3.util.SplitConfigurationOptions.StageType; +import com.android.launcher3.util.TraceHelper; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskKey; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.wm.shell.splitscreen.ISplitScreenListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * This class tracked the top-most task and some 'approximate' task history to allow faster + * system state estimation during touch interaction + */ +public class TopTaskTracker extends ISplitScreenListener.Stub implements TaskStackChangeListener { + + public static MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(TopTaskTracker::new); + + private static final int HISTORY_SIZE = 5; + + // Ordered list with first item being the most recent task. + private final LinkedList mOrderedTaskList = new LinkedList<>(); + + + private final SplitStageInfo mMainStagePosition = new SplitStageInfo(); + private final SplitStageInfo mSideStagePosition = new SplitStageInfo(); + private int mPinnedTaskId = INVALID_TASK_ID; + + private TopTaskTracker(Context context) { + mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN; + mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE; + + TaskStackChangeListeners.getInstance().registerTaskStackListener(this); + SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this); + } + + @Override + public void onTaskRemoved(int taskId) { + mOrderedTaskList.removeIf(rto -> rto.taskId == taskId); + } + + @Override + public void onTaskMovedToFront(RunningTaskInfo taskInfo) { + mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId); + mOrderedTaskList.addFirst(taskInfo); + + // Keep the home display's top running task in the first while adding a non-home + // display's task to the list, to avoid showing non-home display's task upon going to + // Recents animation. + if (taskInfo.displayId != DEFAULT_DISPLAY) { + final RunningTaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream() + .filter(rto -> rto.displayId == DEFAULT_DISPLAY).findFirst().orElse(null); + if (topTaskOnHomeDisplay != null) { + mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId); + mOrderedTaskList.addFirst(topTaskOnHomeDisplay); + } + } + + if (mOrderedTaskList.size() >= HISTORY_SIZE) { + // If we grow in size, remove the last taskInfo which is not part of the split task. + Iterator itr = mOrderedTaskList.descendingIterator(); + while (itr.hasNext()) { + RunningTaskInfo info = itr.next(); + if (info.taskId != taskInfo.taskId + && info.taskId != mMainStagePosition.taskId + && info.taskId != mSideStagePosition.taskId) { + itr.remove(); + return; + } + } + } + } + + @Override + public void onStagePositionChanged(@StageType int stage, @StagePosition int position) { + if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { + mMainStagePosition.stagePosition = position; + } else { + mSideStagePosition.stagePosition = position; + } + } + + @Override + public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { + // If task is not visible but we are tracking it, stop tracking it + if (!visible) { + if (mMainStagePosition.taskId == taskId) { + resetTaskId(mMainStagePosition); + } else if (mSideStagePosition.taskId == taskId) { + resetTaskId(mSideStagePosition); + } // else it's an un-tracked child + return; + } + + // If stage has moved to undefined, stop tracking the task + if (stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) { + resetTaskId(taskId == mMainStagePosition.taskId + ? mMainStagePosition : mSideStagePosition); + return; + } + + if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { + mMainStagePosition.taskId = taskId; + } else { + mSideStagePosition.taskId = taskId; + } + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + mPinnedTaskId = taskId; + } + + @Override + public void onActivityUnpinned() { + mPinnedTaskId = INVALID_TASK_ID; + } + + private void resetTaskId(SplitStageInfo taskPosition) { + taskPosition.taskId = -1; + } + + /** + * @return index 0 will be task in left/top position, index 1 in right/bottom position. + * Will return empty array if device is not in staged split + */ + public int[] getRunningSplitTaskIds() { + if (mMainStagePosition.taskId == INVALID_TASK_ID + || mSideStagePosition.taskId == INVALID_TASK_ID) { + return new int[]{}; + } + int[] out = new int[2]; + if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) { + out[0] = mMainStagePosition.taskId; + out[1] = mSideStagePosition.taskId; + } else { + out[1] = mMainStagePosition.taskId; + out[0] = mSideStagePosition.taskId; + } + return out; + } + + + /** + * Returns the CachedTaskInfo for the top most task + */ + @UiThread + public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) { + if (filterOnlyVisibleRecents) { + // Since we only know about the top most task, any filtering may not be applied on the + // cache. The second to top task may change while the top task is still the same. + RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () -> + ActivityManagerWrapper.getInstance().getRunningTasks(true)); + return new CachedTaskInfo(Arrays.asList(tasks)); + } + + if (mOrderedTaskList.isEmpty()) { + RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () -> + ActivityManagerWrapper.getInstance().getRunningTasks( + false /* filterOnlyVisibleRecents */)); + Collections.addAll(mOrderedTaskList, tasks); + } + + // Strip the pinned task + ArrayList tasks = new ArrayList<>(mOrderedTaskList); + tasks.removeIf(t -> t.taskId == mPinnedTaskId); + return new CachedTaskInfo(tasks); + } + + /** + * Class to provide information about a task which can be safely cached and do not change + * during the lifecycle of the task. + */ + public static class CachedTaskInfo { + + @Nullable + private final RunningTaskInfo mTopTask; + private final List mAllCachedTasks; + + CachedTaskInfo(List allCachedTasks) { + mAllCachedTasks = allCachedTasks; + mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0); + } + + public int getTaskId() { + return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId; + } + + /** + * Returns true if the root of the task chooser activity + */ + public boolean isRootChooseActivity() { + return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction()); + } + + /** + * Returns true if the given task holds an Assistant activity that is excluded from recents + */ + public boolean isExcludedAssistant() { + return mTopTask != null && mTopTask.configuration.windowConfiguration + .getActivityType() == ACTIVITY_TYPE_ASSISTANT + && (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; + } + + /** + * Returns true if this represents the HOME task + */ + public boolean isHomeTask() { + return mTopTask != null && mTopTask.configuration.windowConfiguration + .getActivityType() == ACTIVITY_TYPE_HOME; + } + + /** + * Returns {@code true} if this task windowing mode is set to {@link + * android.app.WindowConfiguration#WINDOWING_MODE_FREEFORM} + */ + public boolean isFreeformTask() { + return mTopTask != null && mTopTask.configuration.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FREEFORM; + } + + /** + * Returns {@link Task} array which can be used as a placeholder until the true object + * is loaded by the model + */ + public Task[] getPlaceholderTasks() { + return mTopTask == null ? new Task[0] + : new Task[] {Task.from(new TaskKey(mTopTask), mTopTask, false)}; + } + + /** + * Returns {@link Task} array corresponding to the provided task ids which can be used as a + * placeholder until the true object is loaded by the model + */ + public Task[] getPlaceholderTasks(int[] taskIds) { + if (mTopTask == null) { + return new Task[0]; + } + Task[] result = new Task[taskIds.length]; + for (int i = 0; i < taskIds.length; i++) { + final int index = i; + int taskId = taskIds[i]; + mAllCachedTasks.forEach(rti -> { + if (rti.taskId == taskId) { + result[index] = Task.from(new TaskKey(rti), rti, false); + } + }); + } + return result; + } + + @UserIdInt + @Nullable + public Integer getUserId() { + return mTopTask == null ? null : mTopTask.userId; + } + + @Nullable + public String getPackageName() { + if (mTopTask == null || mTopTask.baseActivity == null) { + return null; + } + return mTopTask.baseActivity.getPackageName(); + } + } +} diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index e277ce79d9..0506128b47 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -15,53 +15,53 @@ */ package com.android.quickstep; -import static android.content.Intent.ACTION_CHOOSER; +import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.quickstep.GestureState.DEFAULT_STATE; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SPLIT_SCREEN; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_STARTING_WINDOW; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; +import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER; +import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.annotation.TargetApi; import android.app.PendingIntent; import android.app.RemoteAction; import android.app.Service; -import android.content.ComponentName; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; -import android.graphics.Point; -import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Icon; -import android.hardware.display.DisplayManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.SystemClock; -import android.os.SystemProperties; import android.util.Log; import android.view.Choreographer; -import android.view.Display; import android.view.InputEvent; import android.view.MotionEvent; -import android.view.Surface; +import android.view.SurfaceControl; import android.view.accessibility.AccessibilityManager; import androidx.annotation.BinderThread; @@ -69,22 +69,27 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +//import com.android.app.viewcapture.SettingsAwareViewCapture; import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; -import com.android.launcher3.ResourceUtils; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.provider.RestoreDbTask; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarManager; import com.android.launcher3.testing.TestLogging; -import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.tracing.LauncherTraceProto; import com.android.launcher3.tracing.TouchInteractionServiceProto; +import com.android.launcher3.uioverrides.flags.FlagsFactory; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.util.WindowBounds; import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; import com.android.quickstep.inputconsumers.AssistantInputConsumer; import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer; @@ -98,20 +103,21 @@ import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer; import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer; import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer; import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.util.AssistantUtilities; -import com.android.quickstep.util.LauncherSplitScreenListener; +import com.android.quickstep.util.ActiveGestureLog.CompoundString; import com.android.quickstep.util.ProtoTracer; import com.android.quickstep.util.ProxyScreenStatusProvider; -import com.android.quickstep.util.SplitScreenBounds; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.InputMonitorCompat; -import com.android.systemui.shared.system.smartspace.ISmartspaceTransitionController; +import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController; +import com.android.systemui.shared.testing.ResourceUtils; import com.android.systemui.shared.tracing.ProtoTraceable; +import com.android.systemui.unfold.progress.IUnfoldAnimation; +import com.android.wm.shell.back.IBackAnimation; +import com.android.wm.shell.desktopmode.IDesktopMode; import com.android.wm.shell.onehanded.IOneHanded; import com.android.wm.shell.pip.IPip; import com.android.wm.shell.recents.IRecentTasks; @@ -134,23 +140,12 @@ import app.lawnchair.LawnchairApp; public class TouchInteractionService extends Service implements ProtoTraceable { + private static final String SUBSTRING_PREFIX = "; "; + private static final String NEWLINE_PREFIX = "\n\t\t\t-> "; + private static final String TAG = "TouchInteractionService"; - private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount"; - private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE"; private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once"; - private static final int MAX_BACK_NOTIFICATION_COUNT = 3; - - /** - * System Action ID to show all apps. - * TODO: Use AccessibilityService's corresponding global action constant in S - */ - private static final int SYSTEM_ACTION_ID_ALL_APPS = 14; - - public static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION = - SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false); - - private int mBackGestureNotificationCounter = -1; private final TISBinder mTISBinder = LawnchairApp.isRecentsEnabled() ? new TISBinder() : null; @@ -159,6 +154,9 @@ public class TouchInteractionService extends Service */ public class TISBinder extends IOverviewProxy.Stub { + @Nullable + private Runnable mOnOverviewTargetChangeListener = null; + @BinderThread public void onInitialize(Bundle bundle) { ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface( @@ -172,16 +170,23 @@ public class TouchInteractionService extends Service bundle.getBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS)); IStartingWindow startingWindow = IStartingWindow.Stub.asInterface( bundle.getBinder(KEY_EXTRA_SHELL_STARTING_WINDOW)); - ISmartspaceTransitionController smartspaceTransitionController = - ISmartspaceTransitionController.Stub.asInterface( - bundle.getBinder(KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER)); + ISysuiUnlockAnimationController launcherUnlockAnimationController = ISysuiUnlockAnimationController.Stub + .asInterface( + bundle.getBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER)); IRecentTasks recentTasks = IRecentTasks.Stub.asInterface( - bundle.getBinder(KEY_EXTRA_RECENT_TASKS)); + bundle.getBinder(KEY_EXTRA_SHELL_RECENT_TASKS)); + IBackAnimation backAnimation = IBackAnimation.Stub.asInterface( + bundle.getBinder(KEY_EXTRA_SHELL_BACK_ANIMATION)); + IDesktopMode desktopMode = IDesktopMode.Stub.asInterface( + bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE)); + IUnfoldAnimation unfoldTransition = IUnfoldAnimation.Stub.asInterface( + bundle.getBinder(KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER)); MAIN_EXECUTOR.execute(() -> { SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip, - splitscreen, onehanded, shellTransitions, startingWindow, recentTasks, - smartspaceTransitionController); - TouchInteractionService.this.initInputMonitor(); + splitscreen, onehanded, shellTransitions, startingWindow, + recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode, + unfoldTransition); + TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()"); preloadOverview(true /* fromInit */); }); sIsInitialized = true; @@ -203,7 +208,7 @@ public class TouchInteractionService extends Service public void onOverviewShown(boolean triggeredFromAltTab) { if (triggeredFromAltTab) { TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW_NEXT_FOCUS); + mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_KEYBOARD_INPUT); } else { mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW); } @@ -220,16 +225,12 @@ public class TouchInteractionService extends Service @BinderThread @Override - public void onTip(int actionType, int viewType) { - // Please delete this method from the interface - } - - @BinderThread - @Override - public void onAssistantAvailable(boolean available) { + public void onAssistantAvailable(boolean available, boolean longPressHomeEnabled) { MAIN_EXECUTOR.execute(() -> { mDeviceState.setAssistantAvailable(available); TouchInteractionService.this.onAssistantVisibilityChanged(); + executeForTaskbarManager(() -> mTaskbarManager + .onLongPressHomeEnabled(longPressHomeEnabled)); }); } @@ -242,10 +243,9 @@ public class TouchInteractionService extends Service }); } - @BinderThread - public void onBackAction(boolean completed, int downX, int downY, boolean isButton, - boolean gestureSwipeLeft) { - // Remove this method from the interface + @Override + public void onNavigationBarSurface(SurfaceControl surface) { + // TODO: implement } @BinderThread @@ -262,18 +262,44 @@ public class TouchInteractionService extends Service MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region)); } - @Override - public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) { - WindowBounds wb = new WindowBounds(bounds, insets); - MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb)); - } - @BinderThread @Override public void onScreenTurnedOn() { MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurnedOn); } + @BinderThread + @Override + public void onScreenTurningOn() { + MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOn); + } + + @BinderThread + @Override + public void onScreenTurningOff() { + MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOff); + } + + @BinderThread + @Override + public void enterStageSplitFromRunningApp(boolean leftOrTop) { + StatefulActivity activity = mOverviewComponentObserver.getActivityInterface().getCreatedActivity(); + if (activity != null) { + activity.enterStageSplitFromRunningApp(leftOrTop); + } + } + + /** + * Preloads the Overview activity. + * + * This method should only be used when the All Set page of the SUW is reached + * to safely + * preload the Launcher for the SUW first reveal. + */ + public void preloadOverviewForSUWAllSet() { + preloadOverview(false, true); + } + @Override public void onRotationProposal(int rotation, boolean isValid) { executeForTaskbarManager(() -> mTaskbarManager.onRotationProposal(rotation, isValid)); @@ -320,6 +346,25 @@ public class TouchInteractionService extends Service public void setSwipeUpProxy(Function proxy) { mSwipeUpProxyProvider = proxy != null ? proxy : (i -> null); } + + /** + * Sets the task id where gestures should be blocked + */ + public void setGestureBlockedTaskId(int taskId) { + mDeviceState.setGestureBlockingTaskId(taskId); + } + + /** Sets a listener to be run on Overview Target updates. */ + public void setOverviewTargetChangeListener(@Nullable Runnable listener) { + mOnOverviewTargetChangeListener = listener; + } + + protected void onOverviewTargetChange() { + if (mOnOverviewTargetChangeListener != null) { + mOnOverviewTargetChangeListener.run(); + mOnOverviewTargetChangeListener = null; + } + } } private static boolean sConnected = false; @@ -334,10 +379,8 @@ public class TouchInteractionService extends Service return sIsInitialized; } - private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory = - this::createLauncherSwipeHandler; - private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory = - this::createFallbackSwipeHandler; + private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory = this::createLauncherSwipeHandler; + private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory = this::createFallbackSwipeHandler; private ActivityManagerWrapper mAM; private OverviewCommandHelper mOverviewCommandHelper; @@ -355,36 +398,35 @@ public class TouchInteractionService extends Service private InputMonitorCompat mInputMonitorCompat; private InputEventReceiver mInputEventReceiver; - private DisplayManager mDisplayManager; - private TaskbarManager mTaskbarManager; private Function mSwipeUpProxyProvider = i -> null; @Override public void onCreate() { super.onCreate(); - if (!LawnchairApp.isRecentsEnabled()) return; + if (!LawnchairApp.isRecentsEnabled()) + return; // Initialize anything here that is needed in direct boot mode. // Everything else should be initialized in onUserUnlocked() below. mMainChoreographer = Choreographer.getInstance(); mAM = ActivityManagerWrapper.getInstance(); mDeviceState = new RecentsAnimationDeviceState(this, true); - mDisplayManager = getSystemService(DisplayManager.class); mTaskbarManager = new TaskbarManager(this); mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); + BootAwarePreloader.start(this); - // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized. + // Call runOnUserUnlocked() before any other callbacks to ensure everything is + // initialized. mDeviceState.runOnUserUnlocked(this::onUserUnlocked); mDeviceState.runOnUserUnlocked(mTaskbarManager::onUserUnlocked); mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged); - mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged); ProtoTracer.INSTANCE.get(this).add(this); - LauncherSplitScreenListener.INSTANCE.get(this).init(); sConnected = true; } - private void disposeEventHandlers() { + private void disposeEventHandlers(String reason) { + Log.d(TAG, "disposeEventHandlers: Reason: " + reason); if (mInputEventReceiver != null) { mInputEventReceiver.dispose(); mInputEventReceiver = null; @@ -395,23 +437,14 @@ public class TouchInteractionService extends Service } } - private void initInputMonitor() { - disposeEventHandlers(); + private void initInputMonitor(String reason) { + disposeEventHandlers("Initializing input monitor due to: " + reason); if (mDeviceState.isButtonNavMode()) { return; } - if (Utilities.ATLEAST_S) { - mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId()); - } else { - if (!SystemUiProxy.INSTANCE.get(this).isActive()) { - return; - } - Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up", - mDeviceState.getDisplayId()); - mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR); - } + mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId()); mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(), mMainChoreographer, this::onInputEvent); @@ -419,36 +452,32 @@ public class TouchInteractionService extends Service } /** - * Called when the navigation mode changes, guaranteed to be after the device state has updated. + * Called when the navigation mode changes, guaranteed to be after the device + * state has updated. */ - private void onNavigationModeChanged(SysUINavigationMode.Mode mode) { - initInputMonitor(); + private void onNavigationModeChanged() { + initInputMonitor("onNavigationModeChanged()"); resetHomeBounceSeenOnQuickstepEnabledFirstTime(); } - /** - * Called when the one handed mode overlay package changes, to recreate touch region. - */ - private void onOneHandedModeOverlayChanged(int newGesturalHeight) { - initInputMonitor(); - } - @UiThread public void onUserUnlocked() { mTaskAnimationManager = new TaskAnimationManager(this); mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState); mOverviewCommandHelper = new OverviewCommandHelper(this, mOverviewComponentObserver, mTaskAnimationManager); - mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager); + mResetGestureInputConsumer = new ResetGestureInputConsumer( + mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); mInputConsumer.registerInputConsumer(); onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags()); onAssistantVisibilityChanged(); + // Initialize the task tracker + TopTaskTracker.INSTANCE.get(this); + // Temporarily disable model preload // new ModelPreload().start(this); - mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this) - .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT)); resetHomeBounceSeenOnQuickstepEnabledFirstTime(); mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange); @@ -461,13 +490,14 @@ public class TouchInteractionService extends Service private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) { - // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation + // Skip if not yet unlocked (can't read user shared prefs) or if the current + // navigation // mode doesn't have gestures return; } // Reset home bounce seen on quick step enabled for first time - SharedPreferences sharedPrefs = Utilities.getPrefs(this); + SharedPreferences sharedPrefs = LauncherPrefs.getPrefs(this); if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) { sharedPrefs.edit() .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true) @@ -486,11 +516,11 @@ public class TouchInteractionService extends Service Icon.createWithResource(this, R.drawable.ic_apps), getString(R.string.all_apps_label), getString(R.string.all_apps_label), - PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent, + PendingIntent.getActivity(this, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); - am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS); + am.registerSystemAction(allAppsAction, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); } else { - am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS); + am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); } StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface() @@ -498,6 +528,7 @@ public class TouchInteractionService extends Service if (newOverviewActivity != null) { mTaskbarManager.setActivity(newOverviewActivity); } + mTISBinder.onOverviewTargetChange(); } @UiThread @@ -508,17 +539,26 @@ public class TouchInteractionService extends Service mOverviewComponentObserver.onSystemUiStateChanged(); mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags); - boolean wasExpanded = (lastSysUIFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0; - boolean isExpanded = - (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0; + boolean wasFreeformActive = (lastSysUIFlags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0; + boolean isFreeformActive = (systemUiStateFlags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0; + if (wasFreeformActive != isFreeformActive) { + DesktopVisibilityController controller = LauncherActivityInterface.INSTANCE + .getDesktopVisibilityController(); + if (controller != null) { + controller.setFreeformTasksVisible(isFreeformActive); + } + } + + int isShadeExpandedFlag = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; + boolean wasExpanded = (lastSysUIFlags & isShadeExpandedFlag) != 0; + boolean isExpanded = (systemUiStateFlags & isShadeExpandedFlag) != 0; if (wasExpanded != isExpanded && isExpanded) { // End live tile when expanding the notification panel for the first time from // overview. mTaskAnimationManager.endLiveTile(); } - if ((lastSysUIFlags & SYSUI_STATE_TRACING_ENABLED) != - (systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED)) { + if ((lastSysUIFlags & SYSUI_STATE_TRACING_ENABLED) != (systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED)) { // Update the tracing state if ((systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED) != 0) { Log.d(TAG, "Starting tracing."); @@ -552,16 +592,15 @@ public class TouchInteractionService extends Service mInputConsumer.unregisterInputConsumer(); mOverviewComponentObserver.onDestroy(); } - disposeEventHandlers(); + disposeEventHandlers("TouchInteractionService onDestroy()"); mDeviceState.destroy(); SystemUiProxy.INSTANCE.get(this).clearProxy(); ProtoTracer.INSTANCE.get(this).stop(); ProtoTracer.INSTANCE.get(this).remove(this); getSystemService(AccessibilityManager.class) - .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS); + .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); - LauncherSplitScreenListener.INSTANCE.get(this).destroy(); mTaskbarManager.destroy(); sConnected = false; super.onDestroy(); @@ -579,15 +618,6 @@ public class TouchInteractionService extends Service return; } MotionEvent event = (MotionEvent) ev; - if (ENABLE_PER_WINDOW_INPUT_ROTATION) { - final Display display = mDisplayManager.getDisplay(mDeviceState.getDisplayId()); - int rotation = display.getRotation(); - Point sz = new Point(); - display.getRealSize(sz); - if (rotation != Surface.ROTATION_0) { - event.transform(InputChannelCompat.createRotationMatrix(rotation, sz.x, sz.y)); - } - } TestLogging.recordMotionEvent( TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event); @@ -605,7 +635,8 @@ public class TouchInteractionService extends Service if (!mDeviceState.isOneHandedModeActive() && mRotationTouchHelper.isInSwipeUpTouchRegion(event)) { - // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger + // Clone the previous gesture state since onConsumerAboutToBeSwitched might + // trigger // onConsumerInactive and wipe the previous gesture state GestureState prevGestureState = new GestureState(mGestureState); GestureState newGestureState = createGestureState(mGestureState); @@ -613,8 +644,6 @@ public class TouchInteractionService extends Service mConsumer.onConsumerAboutToBeSwitched(); mGestureState = newGestureState; mConsumer = newConsumer(prevGestureState, mGestureState, event); - - ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName()); mUncheckedConsumer = mConsumer; } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode() && mDeviceState.canTriggerAssistantAction(event)) { @@ -622,8 +651,7 @@ public class TouchInteractionService extends Service // 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. - mUncheckedConsumer = tryCreateAssistantInputConsumer( - InputConsumer.NO_OP, mGestureState, event); + mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event); } else if (mDeviceState.canTriggerOneHandedAction(event)) { // Consume gesture event for triggering one handed feature. mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState, @@ -643,12 +671,17 @@ public class TouchInteractionService extends Service switch (event.getActionMasked()) { case ACTION_DOWN: case ACTION_UP: - ActiveGestureLog.INSTANCE.addLog("onMotionEvent(" - + (int) event.getRawX() + ", " + (int) event.getRawY() + ")", - event.getActionMasked()); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "onMotionEvent(" + (int) event.getRawX() + ", " + + (int) event.getRawY() + "): " + + MotionEvent.actionToString(event.getActionMasked()), + /* gestureEvent= */ event.getActionMasked() == ACTION_DOWN + ? MOTION_DOWN + : MOTION_UP); break; default: - ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked()); + ActiveGestureLog.INSTANCE.addLog("onMotionEvent: " + + MotionEvent.actionToString(event.getActionMasked())); break; } } @@ -670,154 +703,293 @@ public class TouchInteractionService extends Service ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate(); } - private InputConsumer tryCreateAssistantInputConsumer(InputConsumer base, + private InputConsumer tryCreateAssistantInputConsumer( GestureState gestureState, MotionEvent motionEvent) { - return mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask()) - ? base - : new AssistantInputConsumer(this, gestureState, base, mInputMonitorCompat, - mDeviceState, motionEvent); + return tryCreateAssistantInputConsumer( + InputConsumer.NO_OP, gestureState, motionEvent, CompoundString.NO_OP); + } + + private InputConsumer tryCreateAssistantInputConsumer( + InputConsumer base, + GestureState gestureState, + MotionEvent motionEvent, + CompoundString reasonString) { + if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) { + reasonString.append(SUBSTRING_PREFIX) + .append("is gesture-blocked task, using base input consumer"); + return base; + } else { + reasonString.append(SUBSTRING_PREFIX).append("using AssistantInputConsumer"); + return new AssistantInputConsumer( + this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent); + } } public GestureState createGestureState(GestureState previousGestureState) { - GestureState gestureState = new GestureState(mOverviewComponentObserver, - ActiveGestureLog.INSTANCE.generateAndSetLogId()); + final GestureState gestureState; + TopTaskTracker.CachedTaskInfo taskInfo; if (mTaskAnimationManager.isRecentsAnimationRunning()) { - gestureState.updateRunningTask(previousGestureState.getRunningTask()); + gestureState = new GestureState(mOverviewComponentObserver, + ActiveGestureLog.INSTANCE.getLogId()); + taskInfo = previousGestureState.getRunningTask(); + gestureState.updateRunningTask(taskInfo); gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId()); gestureState.updatePreviouslyAppearedTaskIds( previousGestureState.getPreviouslyAppearedTaskIds()); } else { - gestureState.updateRunningTasks(TraceHelper.allowIpcs("getRunningTask.0", - () -> mAM.getRunningTasks(false /* filterOnlyVisibleRecents */))); + gestureState = new GestureState(mOverviewComponentObserver, + ActiveGestureLog.INSTANCE.incrementLogId()); + taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false); + gestureState.updateRunningTask(taskInfo); } + // Log initial state for the gesture. + ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=") + .append(taskInfo == null ? "no running task" : taskInfo.getPackageName())); + ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current SystemUi state flags=") + .append(mDeviceState.getSystemUiStateString())); return gestureState; } - private InputConsumer newConsumer(GestureState previousGestureState, - GestureState newGestureState, MotionEvent event) { + private InputConsumer newConsumer( + GestureState previousGestureState, GestureState newGestureState, MotionEvent event) { AnimatedFloat progressProxy = mSwipeUpProxyProvider.apply(mGestureState); if (progressProxy != null) { - return new ProgressDelegateInputConsumer(this, mTaskAnimationManager, - mGestureState, mInputMonitorCompat, progressProxy); + InputConsumer consumer = new ProgressDelegateInputConsumer( + this, mTaskAnimationManager, mGestureState, mInputMonitorCompat, progressProxy); + + logInputConsumerSelectionReason(consumer, newCompoundString( + "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer")); + + return consumer; } boolean canStartSystemGesture = mDeviceState.canStartSystemGesture(); if (!mDeviceState.isUserUnlocked()) { + CompoundString reasonString = newCompoundString("device locked"); + InputConsumer consumer; if (canStartSystemGesture) { // This handles apps launched in direct boot mode (e.g. dialer) as well as apps - // launched while device is locked even after exiting direct boot mode (e.g. camera). - return createDeviceLockedInputConsumer(newGestureState); + // launched while device is locked even after exiting direct boot mode (e.g. + // camera). + consumer = createDeviceLockedInputConsumer( + newGestureState, reasonString.append(SUBSTRING_PREFIX) + .append("can start system gesture")); } else { - return getDefaultInputConsumer(); + consumer = getDefaultInputConsumer( + reasonString.append(SUBSTRING_PREFIX) + .append("cannot start system gesture")); } + logInputConsumerSelectionReason(consumer, reasonString); + return consumer; } - // When there is an existing recents animation running, bypass systemState check as this is + CompoundString reasonString; + InputConsumer base; + // When there is an existing recents animation running, bypass systemState check + // as this is // a followup gesture and the first gesture started in a valid system state. - InputConsumer base = canStartSystemGesture - || previousGestureState.isRecentsAnimationRunning() - ? newBaseConsumer(previousGestureState, newGestureState, event) - : getDefaultInputConsumer(); + if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning()) { + reasonString = newCompoundString(canStartSystemGesture + ? "can start system gesture" + : "recents animation was running") + .append(", trying to use base consumer"); + base = newBaseConsumer(previousGestureState, newGestureState, event, reasonString); + } else { + reasonString = newCompoundString( + "cannot start system gesture and recents animation was not running") + .append(", trying to use default input consumer"); + base = getDefaultInputConsumer(reasonString); + } if (mDeviceState.isGesturalNavMode()) { handleOrientationSetup(base); } if (mDeviceState.isFullyGesturalNavMode()) { + String reasonPrefix = "device is in gesture navigation mode"; if (mDeviceState.canTriggerAssistantAction(event)) { - base = tryCreateAssistantInputConsumer(base, newGestureState, event); + reasonString.append(NEWLINE_PREFIX) + .append(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("gesture can trigger the assistant") + .append(", trying to use assistant input consumer"); + base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString); } // If Taskbar is present, we listen for long press to unstash it. - BaseActivityInterface activityInterface = newGestureState.getActivityInterface(); - StatefulActivity activity = activityInterface.getCreatedActivity(); - if (activity != null && activity.getDeviceProfile().isTaskbarPresent) { - base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, - mTaskbarManager.getCurrentActivityContext()); + TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); + if (tac != null && canStartSystemGesture) { + reasonString.append(NEWLINE_PREFIX) + .append(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("TaskbarActivityContext != null, using TaskbarStashInputConsumer"); + base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, tac); } - // If Bubbles is expanded, use the overlay input consumer, which will close Bubbles - // instead of going all the way home when a swipe up is detected. - if (mDeviceState.isBubblesExpanded() || mDeviceState.isGlobalActionsShowing()) { + if (mDeviceState.isBubblesExpanded()) { + reasonString = newCompoundString(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("bubbles expanded, trying to use default input consumer"); + // Bubbles can handle home gesture itself. + base = getDefaultInputConsumer(reasonString); + } + + if (mDeviceState.isSystemUiDialogShowing()) { + reasonString = newCompoundString(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("system dialog is showing, using SysUiOverlayInputConsumer"); base = new SysUiOverlayInputConsumer( getBaseContext(), mDeviceState, mInputMonitorCompat); } if (mDeviceState.isScreenPinningActive()) { + reasonString = newCompoundString(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("screen pinning is active, using ScreenPinnedInputConsumer"); // Note: we only allow accessibility to wrap this, and it replaces the previous - // base input consumer (which should be NO_OP anyway since topTaskLocked == true). + // base input consumer (which should be NO_OP anyway since topTaskLocked == + // true). base = new ScreenPinnedInputConsumer(this, newGestureState); } if (mDeviceState.canTriggerOneHandedAction(event)) { - base = new OneHandedModeInputConsumer(this, mDeviceState, base, - mInputMonitorCompat); + reasonString.append(NEWLINE_PREFIX) + .append(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("gesture can trigger one handed mode") + .append(", using OneHandedModeInputConsumer"); + base = new OneHandedModeInputConsumer( + this, mDeviceState, base, mInputMonitorCompat); } if (mDeviceState.isAccessibilityMenuAvailable()) { - base = new AccessibilityInputConsumer(this, mDeviceState, base, - mInputMonitorCompat); + reasonString.append(NEWLINE_PREFIX) + .append(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("accessibility menu is available") + .append(", using AccessibilityInputConsumer"); + base = new AccessibilityInputConsumer( + this, mDeviceState, base, mInputMonitorCompat); } } else { + String reasonPrefix = "device is not in gesture navigation mode"; if (mDeviceState.isScreenPinningActive()) { - base = getDefaultInputConsumer(); + reasonString = newCompoundString(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("screen pinning is active, trying to use default input consumer"); + base = getDefaultInputConsumer(reasonString); } if (mDeviceState.canTriggerOneHandedAction(event)) { - base = new OneHandedModeInputConsumer(this, mDeviceState, base, - mInputMonitorCompat); + reasonString.append(NEWLINE_PREFIX) + .append(reasonPrefix) + .append(SUBSTRING_PREFIX) + .append("gesture can trigger one handed mode") + .append(", using OneHandedModeInputConsumer"); + base = new OneHandedModeInputConsumer( + this, mDeviceState, base, mInputMonitorCompat); } } + logInputConsumerSelectionReason(base, reasonString); return base; } + private CompoundString newCompoundString(String substring) { + return new CompoundString(NEWLINE_PREFIX).append(substring); + } + + private void logInputConsumerSelectionReason( + InputConsumer consumer, CompoundString reasonString) { + if (!FeatureFlags.ENABLE_INPUT_CONSUMER_REASON_LOGGING.get()) { + ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + consumer.getName()); + return; + } + ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ") + .append(consumer.getName()) + .append(". reason(s):") + .append(reasonString)); + if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) { + ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER); + } + } + private void handleOrientationSetup(InputConsumer baseInputConsumer) { baseInputConsumer.notifyOrientationSetup(); } - private InputConsumer newBaseConsumer(GestureState previousGestureState, - GestureState gestureState, MotionEvent event) { + private InputConsumer newBaseConsumer( + GestureState previousGestureState, + GestureState gestureState, + MotionEvent event, + CompoundString reasonString) { if (mDeviceState.isKeyguardShowingOccluded()) { // This handles apps showing over the lockscreen (e.g. camera) - return createDeviceLockedInputConsumer(gestureState); + return createDeviceLockedInputConsumer( + gestureState, + reasonString.append(SUBSTRING_PREFIX) + .append("keyguard is showing occluded") + .append(", trying to use device locked input consumer")); } + reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded"); // Use overview input consumer for sharesheets on top of home. boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted() && gestureState.getRunningTask() != null - && ACTION_CHOOSER.equals(gestureState.getRunningTask().baseIntent.getAction()); - if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) { - // In the case where we are in the excluded assistant state, ignore it and treat the + && gestureState.getRunningTask().isRootChooseActivity(); + if (gestureState.getRunningTask() != null + && gestureState.getRunningTask().isExcludedAssistant()) { + // In the case where we are in the excluded assistant state, ignore it and treat + // the // running activity as the task behind the assistant - gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.assistant", - () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */))); - ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent(); - ComponentName runningComponent = - gestureState.getRunningTask().baseIntent.getComponent(); - forceOverviewInputConsumer = - runningComponent != null && runningComponent.equals(homeComponent); + gestureState.updateRunningTask(TopTaskTracker.INSTANCE.get(this) + .getCachedTopTask(true /* filterOnlyVisibleRecents */)); + forceOverviewInputConsumer = gestureState.getRunningTask().isHomeTask(); } - if (ENABLE_QUICKSTEP_LIVE_TILE.get() - && gestureState.getActivityInterface().isInLiveTileMode()) { + boolean previousGestureAnimatedToLauncher = previousGestureState.isRunningAnimationToLauncher(); + // with shell-transitions, home is resumed during recents animation, so + // explicitly check against recents animation too. + boolean launcherResumedThroughShellTransition = gestureState.getActivityInterface().isResumed() + && !previousGestureState.isRecentsAnimationRunning(); + if (gestureState.getActivityInterface().isInLiveTileMode()) { return createOverviewInputConsumer( - previousGestureState, gestureState, event, forceOverviewInputConsumer); + previousGestureState, + gestureState, + event, + forceOverviewInputConsumer, + reasonString.append(SUBSTRING_PREFIX) + .append("is in live tile mode, trying to use overview input consumer")); } else if (gestureState.getRunningTask() == null) { - return getDefaultInputConsumer(); - } else if (previousGestureState.isRunningAnimationToLauncher() - || gestureState.getActivityInterface().isResumed() + return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX) + .append("running task == null")); + } else if (previousGestureAnimatedToLauncher + || launcherResumedThroughShellTransition || forceOverviewInputConsumer) { return createOverviewInputConsumer( - previousGestureState, gestureState, event, forceOverviewInputConsumer); - } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) { - return getDefaultInputConsumer(); + previousGestureState, + gestureState, + event, + forceOverviewInputConsumer, + reasonString.append(SUBSTRING_PREFIX) + .append(previousGestureAnimatedToLauncher + ? "previous gesture animated to launcher" + : (launcherResumedThroughShellTransition + ? "launcher resumed through a shell transition" + : "forceOverviewInputConsumer == true")) + .append(", trying to use overview input consumer")); + } else if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) { + return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX) + .append("is gesture-blocked task, trying to use default input consumer")); } else { + reasonString.append(SUBSTRING_PREFIX) + .append("using OtherActivityInputConsumer"); return createOtherActivityInputConsumer(gestureState, event); } } public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() { return !mOverviewComponentObserver.isHomeAndOverviewSame() - ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory; + ? mFallbackSwipeHandlerFactory + : mLauncherSwipeHandlerFactory; } private InputConsumer createOtherActivityInputConsumer(GestureState gestureState, @@ -832,32 +1004,62 @@ public class TouchInteractionService extends Service mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory); } - private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) { + private InputConsumer createDeviceLockedInputConsumer( + GestureState gestureState, CompoundString reasonString) { if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) { - return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager, - gestureState, mInputMonitorCompat); + reasonString.append(SUBSTRING_PREFIX) + .append("device is in gesture nav mode and running task != null") + .append(", using DeviceLockedInputConsumer"); + return new DeviceLockedInputConsumer( + this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat); } else { - return getDefaultInputConsumer(); + return getDefaultInputConsumer(reasonString + .append(SUBSTRING_PREFIX) + .append(mDeviceState.isFullyGesturalNavMode() + ? "running task == null" + : "device is not in gesture nav mode") + .append(", trying to use default input consumer")); } } - public InputConsumer createOverviewInputConsumer(GestureState previousGestureState, - GestureState gestureState, MotionEvent event, - boolean forceOverviewInputConsumer) { + public InputConsumer createOverviewInputConsumer( + GestureState previousGestureState, + GestureState gestureState, + MotionEvent event, + boolean forceOverviewInputConsumer, + CompoundString reasonString) { StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity(); if (activity == null) { - return getDefaultInputConsumer(); + return getDefaultInputConsumer( + reasonString.append(SUBSTRING_PREFIX) + .append("activity == null, trying to use default input consumer")); } - if (activity.getRootView().hasWindowFocus() - || previousGestureState.isRunningAnimationToLauncher() - || (ASSISTANT_GIVES_LAUNCHER_FOCUS.get() - && forceOverviewInputConsumer) - || (ENABLE_QUICKSTEP_LIVE_TILE.get() - && gestureState.getActivityInterface().isInLiveTileMode())) { + boolean hasWindowFocus = activity.getRootView().hasWindowFocus(); + boolean isPreviousGestureAnimatingToLauncher = previousGestureState.isRunningAnimationToLauncher(); + boolean forcingOverviewInputConsumer = ASSISTANT_GIVES_LAUNCHER_FOCUS.get() && forceOverviewInputConsumer; + boolean isInLiveTileMode = gestureState.getActivityInterface().isInLiveTileMode(); + reasonString.append(SUBSTRING_PREFIX) + .append(hasWindowFocus + ? "activity has window focus" + : (isPreviousGestureAnimatingToLauncher + ? "previous gesture is still animating to launcher" + : (forcingOverviewInputConsumer + ? "assistant gives launcher focus and forcing focus" + : (isInLiveTileMode + ? "device is in live mode" + : "all overview focus conditions failed")))); + if (hasWindowFocus + || isPreviousGestureAnimatingToLauncher + || forcingOverviewInputConsumer + || isInLiveTileMode) { + reasonString.append(SUBSTRING_PREFIX) + .append("overview should have focus, using OverviewInputConsumer"); return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat, false /* startingInActivityBounds */); } else { + reasonString.append(SUBSTRING_PREFIX).append( + "overview shouldn't have focus, using OverviewWithoutFocusInputConsumer"); final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event); return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState, mInputMonitorCompat, disableHorizontalSwipe); @@ -865,8 +1067,10 @@ public class TouchInteractionService extends Service } /** - * To be called by the consumer when it's no longer active. This can be called by any consumer - * in the hierarchy at any point during the gesture (ie. if a delegate consumer starts + * To be called by the consumer when it's no longer active. This can be called + * by any consumer + * in the hierarchy at any point during the gesture (ie. if a delegate consumer + * starts * intercepting touches, the base consumer can try to call this). */ private void onConsumerInactive(InputConsumer caller) { @@ -878,27 +1082,42 @@ public class TouchInteractionService extends Service private void reset() { mConsumer = mUncheckedConsumer = getDefaultInputConsumer(); mGestureState = DEFAULT_STATE; - // By default, use batching of the input events, but check receiver before using in the rare + // By default, use batching of the input events, but check receiver before using + // in the rare // case that the monitor was disposed before the swipe settled if (mInputEventReceiver != null) { mInputEventReceiver.setBatchingEnabled(true); } } - /** - * Returns the {@link ResetGestureInputConsumer} if user is unlocked, else NO_OP. - */ private @NonNull InputConsumer getDefaultInputConsumer() { + return getDefaultInputConsumer(CompoundString.NO_OP); + } + + /** + * Returns the {@link ResetGestureInputConsumer} if user is unlocked, else + * NO_OP. + */ + private @NonNull InputConsumer getDefaultInputConsumer(@NonNull CompoundString reasonString) { if (mResetGestureInputConsumer != null) { + reasonString.append(SUBSTRING_PREFIX).append( + "mResetGestureInputConsumer initialized, using ResetGestureInputConsumer"); return mResetGestureInputConsumer; } else { - // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to + reasonString.append(SUBSTRING_PREFIX).append( + "mResetGestureInputConsumer not initialized, using no-op input consumer"); + // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset + // to // NO_OP until then (we never want these to be null). return InputConsumer.NO_OP; } } private void preloadOverview(boolean fromInit) { + preloadOverview(fromInit, false); + } + + private void preloadOverview(boolean fromInit, boolean forSUWAllSet) { if (!mDeviceState.isUserUnlocked()) { return; } @@ -908,23 +1127,29 @@ public class TouchInteractionService extends Service return; } - if (RestoreDbTask.isPending(this) || !mDeviceState.isUserSetupComplete()) { + if ((RestoreDbTask.isPending(this) && !forSUWAllSet) + || !mDeviceState.isUserSetupComplete()) { // Preloading while a restore is pending may cause launcher to start the restore // too early. return; } - final BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); + final BaseActivityInterface activityInterface = mOverviewComponentObserver.getActivityInterface(); final Intent overviewIntent = new Intent( mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState()); if (activityInterface.getCreatedActivity() != null && fromInit) { - // The activity has been created before the initialization of overview service. It is - // usually happens when booting or launcher is the top activity, so we should already + // The activity has been created before the initialization of overview service. + // It is + // usually happens when booting or launcher is the top activity, so we should + // already // have the latest state. return; } + // TODO(b/258022658): Remove temporary logging. + Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet + + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame()); + mTaskAnimationManager.preloadRecentsAnimation(overviewIntent); } @@ -933,8 +1158,7 @@ public class TouchInteractionService extends Service if (!mDeviceState.isUserUnlocked()) { return; } - final BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); + final BaseActivityInterface activityInterface = mOverviewComponentObserver.getActivityInterface(); final BaseDraggingActivity activity = activityInterface.getCreatedActivity(); if (activity == null || activity.isStarted()) { // We only care about the existing background activity. @@ -970,7 +1194,7 @@ public class TouchInteractionService extends Service } } else { // Dump everything - FeatureFlags.dump(pw); + FlagsFactory.dump(pw); if (mDeviceState.isUserUnlocked()) { PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw); } @@ -987,7 +1211,7 @@ public class TouchInteractionService extends Service pw.println("Input state:"); pw.println(" mInputMonitorCompat=" + mInputMonitorCompat); pw.println(" mInputEventReceiver=" + mInputEventReceiver); - SysUINavigationMode.INSTANCE.get(this).dump(pw); + DisplayController.INSTANCE.get(this).dump(pw); pw.println("TouchState:"); BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null : mOverviewComponentObserver.getActivityInterface().getCreatedActivity(); @@ -1000,19 +1224,40 @@ public class TouchInteractionService extends Service RecentsModel.INSTANCE.get(this).dump("", pw); pw.println("ProtoTrace:"); pw.println(" file=" + ProtoTracer.INSTANCE.get(this).getTraceFile()); + if (createdOverviewActivity != null) { + createdOverviewActivity.getDeviceProfile().dump(this, "", pw); + } + mTaskbarManager.dumpLogs("", pw); + +// if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { +// SettingsAwareViewCapture.getInstance(this).dump(pw, fd, this); +// } } } private void printAvailableCommands(PrintWriter pw) { pw.println("Available commands:"); pw.println(" clear-touch-log: Clears the touch interaction log"); + pw.println(" print-gesture-log: only prints the ActiveGestureLog dump"); } private void onCommand(PrintWriter pw, LinkedList args) { - switch (args.pollFirst()) { + String cmd = args.pollFirst(); + if (cmd == null) { + pw.println("Command missing"); + printAvailableCommands(pw); + return; + } + switch (cmd) { case "clear-touch-log": ActiveGestureLog.INSTANCE.clear(); break; + case "print-gesture-log": + ActiveGestureLog.INSTANCE.dump("", pw); + break; + default: + pw.println("Command does not exist: " + cmd); + printAvailableCommands(pw); } } @@ -1032,8 +1277,7 @@ public class TouchInteractionService extends Service @Override public void writeToProto(LauncherTraceProto.Builder proto) { - TouchInteractionServiceProto.Builder serviceProto = - TouchInteractionServiceProto.newBuilder(); + TouchInteractionServiceProto.Builder serviceProto = TouchInteractionServiceProto.newBuilder(); serviceProto.setServiceConnected(true); if (mOverviewComponentObserver != null) { diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java index e290be8e33..b1320674e6 100644 --- a/quickstep/src/com/android/quickstep/ViewUtils.java +++ b/quickstep/src/com/android/quickstep/ViewUtils.java @@ -17,14 +17,13 @@ package com.android.quickstep; import android.graphics.HardwareRenderer; import android.os.Handler; +import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; import com.android.launcher3.Utilities; -import com.android.systemui.shared.system.ViewRootImplCompat; import java.util.function.BooleanSupplier; -import java.util.function.LongConsumer; /** * Utility class for helpful methods related to {@link View} objects. @@ -47,12 +46,15 @@ public class ViewUtils { return new FrameHandler(view, onFinishRunnable, canceled).schedule(); } - private static class FrameHandler implements HardwareRenderer.FrameDrawingCallback { + private static class FrameHandler implements HardwareRenderer.FrameDrawingCallback, + ViewRootImpl.SurfaceChangedCallback { final ViewRootImpl mViewRoot; final Runnable mFinishCallback; final BooleanSupplier mCancelled; final Handler mHandler; + boolean mSurfaceCallbackRegistered = false; + boolean mFinished; int mDeferFrameCount = 1; @@ -63,6 +65,23 @@ public class ViewUtils { mHandler = new Handler(); } + @Override + public void surfaceCreated(SurfaceControl.Transaction t) { + // Do nothing + } + + @Override + public void surfaceReplaced(SurfaceControl.Transaction t) { + // Do nothing + } + + @Override + public void surfaceDestroyed() { + // If the root view is detached, then the app won't get any scheduled frames so we need + // to force-run any pending callbacks + finish(); + } + @Override public void onFrameDraw(long frame) { Utilities.postAsyncCallback(mHandler, this::onFrame); @@ -79,18 +98,35 @@ public class ViewUtils { return; } - if (mFinishCallback != null) { - mFinishCallback.run(); - } + finish(); } private boolean schedule() { - if (mViewRoot.getView() != null) { + if (mViewRoot != null && mViewRoot.getView() != null) { + if (!mSurfaceCallbackRegistered) { + mSurfaceCallbackRegistered = true; + mViewRoot.addSurfaceChangedCallback(this); + } mViewRoot.registerRtFrameCallback(this); mViewRoot.getView().invalidate(); return true; } return false; } + + private void finish() { + if (mFinished) { + return; + } + mFinished = true; + mDeferFrameCount = 0; + if (mFinishCallback != null) { + mFinishCallback.run(); + } + if (mViewRoot != null) { + mViewRoot.removeSurfaceChangedCallback(this); + mSurfaceCallbackRegistered = false; + } + } } } diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java index 9d9ef94169..8a87f63aaf 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java @@ -22,9 +22,9 @@ import androidx.annotation.Nullable; import com.android.launcher3.Utilities; import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.TouchController; import com.android.quickstep.RecentsActivity; -import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.util.NavBarPosition; import com.android.quickstep.util.TriggerSwipeUpTouchTracker; @@ -40,8 +40,8 @@ public class FallbackNavBarTouchController implements TouchController, public FallbackNavBarTouchController(RecentsActivity activity) { mActivity = activity; - SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity); - if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) { + NavigationMode sysUINavigationMode = DisplayController.getNavigationMode(mActivity); + if (sysUINavigationMode == NavigationMode.NO_BUTTON) { NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode, DisplayController.INSTANCE.get(mActivity).getInfo()); mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity, diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java index ff175f18f2..11b1ab8ec9 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java @@ -33,6 +33,7 @@ import static com.android.quickstep.views.RecentsView.TASK_MODALNESS; import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; +import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA; import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL; import android.util.FloatProperty; @@ -44,7 +45,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; -import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiPropertyFactory; import com.android.quickstep.RecentsActivity; import com.android.quickstep.views.ClearAllButton; @@ -77,6 +78,11 @@ public class FallbackRecentsStateController implements StateHandler mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL)); + setter.addEndListener(success -> { + if (!success) { + mRecentsView.reset(); + } + }); mRecentsView.updateEmptyMessage(); setProperties(toState, config, setter); @@ -89,7 +95,7 @@ public class FallbackRecentsStateController implements StateHandler taskViewsFloat = mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset( TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, mActivity.getDeviceProfile()); + setter.setFloat(mRecentsView, taskViewsFloat.first, isSplitSelectionState(state) + ? mRecentsView.getSplitSelectTranslation() : 0, LINEAR); setter.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR); - if (isSplitSelectionState(state)) { - mRecentsView.applySplitPrimaryScrollOffset(); - setter.setFloat(mRecentsView, taskViewsFloat.first, - mRecentsView.getSplitSelectTranslation(), LINEAR); - } else { - mRecentsView.resetSplitPrimaryScrollOffset(); - setter.setFloat(mRecentsView, taskViewsFloat.first, 0, LINEAR); - } } /** diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index d7da74b5c6..4b1dd439f3 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -15,7 +15,8 @@ */ package com.android.quickstep.fallback; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; import static com.android.quickstep.fallback.RecentsState.DEFAULT; import static com.android.quickstep.fallback.RecentsState.HOME; @@ -24,11 +25,9 @@ import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT; import android.animation.AnimatorSet; import android.annotation.TargetApi; -import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.view.MotionEvent; import androidx.annotation.Nullable; @@ -36,11 +35,14 @@ import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.statemanager.StateManager.StateListener; import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.quickstep.FallbackActivityInterface; import com.android.quickstep.GestureState; import com.android.quickstep.RecentsActivity; +import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.TaskViewSimulator; @@ -48,7 +50,6 @@ import com.android.quickstep.views.OverviewActionsView; 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.recents.model.Task.TaskKey; import java.util.ArrayList; @@ -56,7 +57,10 @@ import java.util.ArrayList; public class FallbackRecentsView extends RecentsView implements StateListener { - private RunningTaskInfo mHomeTaskInfo; + private static final int TASK_DISMISS_DURATION = 150; + + @Nullable + private Task mHomeTask; public FallbackRecentsView(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -85,11 +89,12 @@ public class FallbackRecentsView extends RecentsView 0 ? homeTask[0] : null; + onGestureAnimationStart(homeTask, rotationTouchHelper); } /** @@ -102,11 +107,12 @@ public class FallbackRecentsView extends RecentsView setCurrentTask(-1)); AnimatorPlaybackController controller = pa.createPlaybackController(); controller.dispatchOnStart(); @@ -128,8 +134,8 @@ public class FallbackRecentsView extends RecentsView 1) { + protected boolean shouldAddStubTaskView(Task[] runningTasks) { + if (runningTasks.length > 1) { // can't be in split screen w/ home task - return super.shouldAddStubTaskView(runningTaskInfos); + return super.shouldAddStubTaskView(runningTasks); } - RunningTaskInfo runningTaskInfo = runningTaskInfos[0]; - if (mHomeTaskInfo != null && runningTaskInfo != null && - mHomeTaskInfo.taskId == runningTaskInfo.taskId + Task runningTask = runningTasks[0]; + if (mHomeTask != null && runningTask != null + && mHomeTask.key.id == runningTask.key.id && getTaskViewCount() == 0 && mLoadPlanEverApplied) { // Do not add a stub task if we are running over home with empty recents, so that we // show the empty recents message instead of showing a stub task and later removing it. // Ignore empty task signal if applyLoadPlan has never run. return false; } - return super.shouldAddStubTaskView(runningTaskInfos); + return super.shouldAddStubTaskView(runningTasks); } @Override @@ -166,7 +172,8 @@ public class FallbackRecentsView extends RecentsView newList = new ArrayList<>(taskGroups.size() + 1); newList.addAll(taskGroups); - newList.add(new GroupTask( - Task.from(new TaskKey(mHomeTaskInfo), mHomeTaskInfo, false), - null, null)); + newList.add(new GroupTask(mHomeTask, null, null)); taskGroups = newList; } } @@ -189,7 +194,7 @@ public class FallbackRecentsView extends RecentsView

+ * To use this class: + * 1. Create an instance in the target view. + * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call + * {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}. + * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call + * {@link BorderAnimator#setBorderVisible(boolean)} where appropriate. + */ +public final class BorderAnimator { + + public static final int DEFAULT_BORDER_COLOR = 0xffffffff; + + private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300; + private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133; + private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE; + + @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat( + this::updateOutline); + @NonNull private final Rect mBorderBounds = new Rect(); + @NonNull private final BorderBoundsBuilder mBorderBoundsBuilder; + @Px private final int mBorderWidthPx; + @Px private final int mBorderRadiusPx; + @NonNull private final Runnable mInvalidateViewCallback; + private final long mAppearanceDurationMs; + private final long mDisappearanceDurationMs; + @NonNull private final Interpolator mInterpolator; + @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private int mAlignmentAdjustment; + + @Nullable private Animator mRunningBorderAnimation; + + public BorderAnimator( + @NonNull BorderBoundsBuilder borderBoundsBuilder, + int borderWidthPx, + int borderRadiusPx, + @ColorInt int borderColor, + @NonNull Runnable invalidateViewCallback) { + this(borderBoundsBuilder, + borderWidthPx, + borderRadiusPx, + borderColor, + invalidateViewCallback, + DEFAULT_APPEARANCE_ANIMATION_DURATION_MS, + DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS, + DEFAULT_INTERPOLATOR); + } + + public BorderAnimator( + @NonNull BorderBoundsBuilder borderBoundsBuilder, + int borderWidthPx, + int borderRadiusPx, + @ColorInt int borderColor, + @NonNull Runnable invalidateViewCallback, + long appearanceDurationMs, + long disappearanceDurationMs, + @NonNull Interpolator interpolator) { + mBorderBoundsBuilder = borderBoundsBuilder; + mBorderWidthPx = borderWidthPx; + mBorderRadiusPx = borderRadiusPx; + mInvalidateViewCallback = invalidateViewCallback; + mAppearanceDurationMs = appearanceDurationMs; + mDisappearanceDurationMs = disappearanceDurationMs; + mInterpolator = interpolator; + + mBorderPaint.setColor(borderColor); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAlpha(0); + } + + private void updateOutline() { + float interpolatedProgress = mInterpolator.getInterpolation( + mBorderAnimationProgress.value); + mAlignmentAdjustment = (int) Utilities.mapBoundToRange( + mBorderAnimationProgress.value, + /* lowerBound= */ 0f, + /* upperBound= */ 1f, + /* toMin= */ 0f, + /* toMax= */ (float) (mBorderWidthPx / 2f), + mInterpolator); + + mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress)); + mBorderPaint.setStrokeWidth(Math.round(mBorderWidthPx * interpolatedProgress)); + mInvalidateViewCallback.run(); + } + + /** + * Draws the border on the given canvas. + *

+ * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after + * calling super. + */ + public void drawBorder(Canvas canvas) { + canvas.drawRoundRect( + /* left= */ mBorderBounds.left + mAlignmentAdjustment, + /* top= */ mBorderBounds.top + mAlignmentAdjustment, + /* right= */ mBorderBounds.right - mAlignmentAdjustment, + /* bottom= */ mBorderBounds.bottom - mAlignmentAdjustment, + /* rx= */ mBorderRadiusPx - mAlignmentAdjustment, + /* ry= */ mBorderRadiusPx - mAlignmentAdjustment, + /* paint= */ mBorderPaint); + } + + /** + * Builds the border appearance/disappearance animation. + */ + @NonNull + public Animator buildAnimator(boolean isAppearing) { + mBorderBoundsBuilder.updateBorderBounds(mBorderBounds); + mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f); + mRunningBorderAnimation.setDuration( + isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs); + + mRunningBorderAnimation.addListener( + AnimatorListeners.forEndCallback(() -> mRunningBorderAnimation = null)); + + return mRunningBorderAnimation; + } + + /** + * Immediately shows/hides the border without an animation. + * + * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)} + */ + public void setBorderVisible(boolean visible) { + if (mRunningBorderAnimation != null) { + mRunningBorderAnimation.end(); + } + mBorderAnimationProgress.updateValue(visible ? 1f : 0f); + } + + /** + * Callback to update the border bounds when building this animation. + */ + public interface BorderBoundsBuilder { + + /** + * Sets the given rect to the most up-to-date bounds. + */ + void updateBorderBounds(Rect rect); + } +} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java new file mode 100644 index 0000000000..b3f5d82637 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import androidx.annotation.NonNull; + +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task; + +import java.util.ArrayList; + +/** + * A {@link Task} container that can contain N number of tasks that are part of the desktop in + * recent tasks list. + */ +public class DesktopTask extends GroupTask { + + @NonNull + public final ArrayList tasks; + + public DesktopTask(@NonNull ArrayList tasks) { + super(tasks.get(0), null, null, TaskView.Type.DESKTOP); + this.tasks = tasks; + } + + @Override + public boolean containsTask(int taskId) { + for (Task task : tasks) { + if (task.key.id == taskId) { + return true; + } + } + return false; + } + + @Override + public boolean hasMultipleTasks() { + return true; + } + + @Override + public DesktopTask copy() { + return new DesktopTask(tasks); + } +} diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java index e2563e398d..2be4f0a519 100644 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ b/quickstep/src/com/android/quickstep/util/GroupTask.java @@ -19,7 +19,8 @@ package com.android.quickstep.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; +import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; /** @@ -27,23 +28,25 @@ import com.android.systemui.shared.recents.model.Task; * are represented as an app-pair in the recents task list. */ public class GroupTask { - public @NonNull Task task1; - public @Nullable Task task2; - public @Nullable StagedSplitBounds mStagedSplitBounds; + @NonNull + public final Task task1; + @Nullable + public final Task task2; + @Nullable + public final SplitBounds mSplitBounds; + @TaskView.Type + public final int taskViewType; - public GroupTask(@NonNull Task t1, @Nullable Task t2, - @Nullable StagedSplitBounds stagedSplitBounds) { - task1 = t1; - task2 = t2; - mStagedSplitBounds = stagedSplitBounds; + public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) { + this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE); } - public GroupTask(@NonNull GroupTask group) { - task1 = new Task(group.task1); - task2 = group.task2 != null - ? new Task(group.task2) - : null; - mStagedSplitBounds = group.mStagedSplitBounds; + protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds, + @TaskView.Type int taskViewType) { + task1 = t1; + task2 = t2; + mSplitBounds = splitBounds; + this.taskViewType = taskViewType; } public boolean containsTask(int taskId) { @@ -53,4 +56,14 @@ public class GroupTask { public boolean hasMultipleTasks() { return task2 != null; } + + /** + * Create a copy of this instance + */ + public GroupTask copy() { + return new GroupTask( + new Task(task1), + task2 != null ? new Task(task2) : null, + mSplitBounds); + } } diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java index f2d7eaa008..b2f5620905 100644 --- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java +++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java @@ -18,11 +18,14 @@ package com.android.quickstep.util; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; +import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.annotation.UiThread; import android.app.Activity; import android.app.ActivityOptions; import android.app.prediction.AppTarget; @@ -43,15 +46,14 @@ import android.net.Uri; import android.util.Log; import android.view.View; -import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.core.content.FileProvider; import com.android.internal.app.ChooserActivity; +import com.android.internal.util.ScreenshotRequest; import com.android.launcher3.BuildConfig; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.recents.utilities.BitmapUtil; import java.io.File; import java.io.FileOutputStream; @@ -75,77 +77,87 @@ public class ImageActionUtils { * Saves screenshot to location determine by SystemUiProxy */ public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, - Rect screenshotBounds, - Insets visibleInsets, Task.TaskKey task) { - systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot), - screenshotBounds, visibleInsets, task); + Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task) { + ScreenshotRequest request = + new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW) + .setTopComponent(task.sourceComponent) + .setTaskId(task.id) + .setUserId(task.userId) + .setBitmap(screenshot) + .setBoundsOnScreen(screenshotBounds) + .setInsets(visibleInsets) + .build(); + systemUiProxy.takeScreenshot(request); } /** * Launch the activity to share image for overview sharing. This is to share cropped bitmap * with specific share targets (with shortcutInfo and appTarget) rendered in overview. */ - @UiThread public static void shareImage(Context context, Supplier bitmapSupplier, RectF rectF, ShortcutInfo shortcutInfo, AppTarget appTarget, String tag) { - if (bitmapSupplier.get() == null) { - return; - } - Rect crop = new Rect(); - rectF.round(crop); - Intent intent = new Intent(); - Uri uri = getImageUri(bitmapSupplier.get(), crop, context, tag); - ClipData clipdata = new ClipData(new ClipDescription("content", - new String[]{"image/png"}), - new ClipData.Item(uri)); - intent.setAction(Intent.ACTION_SEND) - .setComponent(new ComponentName(appTarget.getPackageName(), appTarget.getClassName())) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(FLAG_GRANT_READ_URI_PERMISSION) - .setType("image/png") - .putExtra(Intent.EXTRA_STREAM, uri) - .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()) - .setClipData(clipdata); + UI_HELPER_EXECUTOR.execute(() -> { + Bitmap bitmap = bitmapSupplier.get(); + if (bitmap == null) { + return; + } + Rect crop = new Rect(); + rectF.round(crop); + Intent intent = new Intent(); + Uri uri = getImageUri(bitmap, crop, context, tag); + ClipData clipdata = new ClipData(new ClipDescription("content", + new String[]{"image/png"}), + new ClipData.Item(uri)); + intent.setAction(Intent.ACTION_SEND) + .setComponent( + new ComponentName(appTarget.getPackageName(), appTarget.getClassName())) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(FLAG_GRANT_READ_URI_PERMISSION) + .setType("image/png") + .putExtra(Intent.EXTRA_STREAM, uri) + .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()) + .setClipData(clipdata); - if (context.getUserId() != appTarget.getUser().getIdentifier()) { - intent.prepareToLeaveUser(context.getUserId()); - intent.fixUris(context.getUserId()); - context.startActivityAsUser(intent, appTarget.getUser()); - } else { - context.startActivity(intent); - } + if (context.getUserId() != appTarget.getUser().getIdentifier()) { + intent.prepareToLeaveUser(context.getUserId()); + intent.fixUris(context.getUserId()); + context.startActivityAsUser(intent, appTarget.getUser()); + } else { + context.startActivity(intent); + } + }); } /** * Launch the activity to share image. */ - @UiThread public static void startShareActivity(Context context, Supplier bitmapSupplier, Rect crop, Intent intent, String tag) { - if (bitmapSupplier.get() == null) { - Log.e(tag, "No snapshot available, not starting share."); - return; - } - - UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context, - bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri, - tag)); + UI_HELPER_EXECUTOR.execute(() -> { + Bitmap bitmap = bitmapSupplier.get(); + if (bitmap == null) { + Log.e(tag, "No snapshot available, not starting share."); + return; + } + persistBitmapAndStartActivity(context, bitmap, crop, intent, + ImageActionUtils::getShareIntentForImageUri, tag); + }); } /** * Launch the activity to share image with shared element transition. */ - @UiThread public static void startShareActivity(Context context, Supplier bitmapSupplier, Rect crop, Intent intent, String tag, View sharedElement) { - if (bitmapSupplier.get() == null) { - Log.e(tag, "No snapshot available, not starting share."); - return; - } - - UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context, - bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri, - tag, sharedElement)); + UI_HELPER_EXECUTOR.execute(() -> { + Bitmap bitmap = bitmapSupplier.get(); + if (bitmap == null) { + Log.e(tag, "No snapshot available, not starting share."); + return; + } + persistBitmapAndStartActivity(context, bitmap, + crop, intent, ImageActionUtils::getShareIntentForImageUri, tag, sharedElement); + }); } @UiThread @@ -167,6 +179,18 @@ public class ImageActionUtils { @WorkerThread public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, Intent intent, BiFunction uriToIntentMap, String tag) { + persistBitmapAndStartActivity(context, bitmap, crop, intent, uriToIntentMap, tag, + (Runnable) null); + } + + /** + * Starts activity based on given intent created from image uri. + * @param exceptionCallback An optional callback to be called when the intent can't be resolved + */ + @WorkerThread + public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, + Intent intent, BiFunction uriToIntentMap, String tag, + Runnable exceptionCallback) { Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); try { @@ -178,6 +202,9 @@ public class ImageActionUtils { } } catch (ActivityNotFoundException e) { Log.e(TAG, "No activity found to receive image intent"); + if (exceptionCallback != null) { + exceptionCallback.run(); + } } } diff --git a/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java b/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java deleted file mode 100644 index 99efb39348..0000000000 --- a/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.android.quickstep.util; - -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; - -import android.content.Context; -import android.os.IBinder; - -import com.android.launcher3.util.MainThreadInitializedObject; -import com.android.launcher3.util.SplitConfigurationOptions; -import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; -import com.android.launcher3.util.SplitConfigurationOptions.StageType; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitTaskPosition; -import com.android.quickstep.SystemUiProxy; -import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.wm.shell.splitscreen.ISplitScreenListener; - -/** - * Listeners for system wide split screen position and stage changes. - * - * Use {@link #getRunningSplitTaskIds()} to determine which tasks, if any, are actively in - * staged split. - * - * Use {@link #getPersistentSplitIds()} to know if tasks were in split screen before a quickswitch - * gesture happened. - */ -public class LauncherSplitScreenListener extends ISplitScreenListener.Stub { - - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(LauncherSplitScreenListener::new); - - private static final int[] EMPTY_ARRAY = {}; - - private final StagedSplitTaskPosition mMainStagePosition = new StagedSplitTaskPosition(); - private final StagedSplitTaskPosition mSideStagePosition = new StagedSplitTaskPosition(); - - private boolean mIsRecentsListFrozen = false; - private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { - @Override - public void onRecentTaskListFrozenChanged(boolean frozen) { - super.onRecentTaskListFrozenChanged(frozen); - mIsRecentsListFrozen = frozen; - - if (frozen) { - mPersistentGroupedIds = getRunningSplitTaskIds(); - } else { - mPersistentGroupedIds = EMPTY_ARRAY; - } - } - }; - - /** - * Gets set to current split taskIDs whenever the task list is frozen, and set to empty array - * whenever task list unfreezes. This also gets set to empty array whenever the user swipes to - * home - in that case the task list does not unfreeze immediately after the gesture, so it's - * done via {@link #notifySwipingToHome()}. - * - * When not empty, this indicates that we need to load a GroupedTaskView as the most recent - * page, so user can quickswitch back to a grouped task. - */ - private int[] mPersistentGroupedIds; - - public LauncherSplitScreenListener(Context context) { - mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN; - mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE; - } - - /** Also call {@link #destroy()} when done. */ - public void init() { - SystemUiProxy.INSTANCE.getNoCreate().registerSplitScreenListener(this); - TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); - } - - public void destroy() { - SystemUiProxy.INSTANCE.getNoCreate().unregisterSplitScreenListener(this); - TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); - } - - /** - * This method returns the active split taskIDs that were active if a user quickswitched from - * split screen to a fullscreen app as long as the recents task list remains frozen. - */ - public int[] getPersistentSplitIds() { - if (mIsRecentsListFrozen) { - return mPersistentGroupedIds; - } else { - return getRunningSplitTaskIds(); - } - } - /** - * @return index 0 will be task in left/top position, index 1 in right/bottom position. - * Will return empty array if device is not in staged split - */ - public int[] getRunningSplitTaskIds() { - if (mMainStagePosition.taskId == -1 || mSideStagePosition.taskId == -1) { - return new int[]{}; - } - int[] out = new int[2]; - if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) { - out[0] = mMainStagePosition.taskId; - out[1] = mSideStagePosition.taskId; - } else { - out[1] = mMainStagePosition.taskId; - out[0] = mSideStagePosition.taskId; - } - return out; - } - - @Override - public void onStagePositionChanged(@StageType int stage, @StagePosition int position) { - if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { - mMainStagePosition.stagePosition = position; - } else { - mSideStagePosition.stagePosition = position; - } - } - - @Override - public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { - // If task is not visible but we are tracking it, stop tracking it - if (!visible) { - if (mMainStagePosition.taskId == taskId) { - resetTaskId(mMainStagePosition); - } else if (mSideStagePosition.taskId == taskId) { - resetTaskId(mSideStagePosition); - } // else it's an un-tracked child - return; - } - - // If stage has moved to undefined, stop tracking the task - if (stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) { - resetTaskId(taskId == mMainStagePosition.taskId ? - mMainStagePosition : mSideStagePosition); - return; - } - - if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { - mMainStagePosition.taskId = taskId; - } else { - mSideStagePosition.taskId = taskId; - } - } - - /** Notifies SystemUi to remove any split screen state */ - public void notifySwipingToHome() { - boolean hasSplitTasks = LauncherSplitScreenListener.INSTANCE.getNoCreate() - .getPersistentSplitIds().length > 0; - if (!hasSplitTasks) { - return; - } - - mPersistentGroupedIds = EMPTY_ARRAY; - } - - private void resetTaskId(StagedSplitTaskPosition taskPosition) { - taskPosition.taskId = -1; - } - - @Override - public IBinder asBinder() { - return this; - } -} diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java index 6b6bd6a8b3..249ea75b9c 100644 --- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java +++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java @@ -15,19 +15,25 @@ */ package com.android.quickstep.util; -import static com.android.launcher3.Utilities.comp; +import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY; +import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION; +import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY; import android.annotation.Nullable; +import android.os.Trace; +import android.util.FloatProperty; +import android.util.MathUtils; import android.view.WindowManager; -import android.view.WindowManagerGlobal; import androidx.core.view.OneShotPreDrawListener; import com.android.launcher3.Hotseat; import com.android.launcher3.Launcher; +import com.android.launcher3.Workspace; import com.android.launcher3.util.HorizontalInsettableView; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener; +import com.android.systemui.unfold.updates.RotationChangeProvider; import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; @@ -38,36 +44,47 @@ public class LauncherUnfoldAnimationController { // Percentage of the width of the quick search bar that will be reduced // from the both sides of the bar when progress is 0 - private static final float MAX_WIDTH_INSET_FRACTION = 0.15f; + private static final float MAX_WIDTH_INSET_FRACTION = 0.04f; + private static final FloatProperty> WORKSPACE_SCALE_PROPERTY = + WORKSPACE_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION); + private static final FloatProperty HOTSEAT_SCALE_PROPERTY = + HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION); private final Launcher mLauncher; + private final ScopedUnfoldTransitionProgressProvider mProgressProvider; + private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider; + private final UnfoldMoveFromCenterHotseatAnimator mUnfoldMoveFromCenterHotseatAnimator; + private final UnfoldMoveFromCenterWorkspaceAnimator mUnfoldMoveFromCenterWorkspaceAnimator; + + private static final String TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION = + "waitingOneFrameBeforeHandlingUnfoldAnimation"; @Nullable private HorizontalInsettableView mQsbInsettable; - private final ScopedUnfoldTransitionProgressProvider mProgressProvider; - private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider; - public LauncherUnfoldAnimationController( Launcher launcher, WindowManager windowManager, - UnfoldTransitionProgressProvider unfoldTransitionProgressProvider) { + UnfoldTransitionProgressProvider unfoldTransitionProgressProvider, + RotationChangeProvider rotationChangeProvider) { mLauncher = launcher; mProgressProvider = new ScopedUnfoldTransitionProgressProvider( unfoldTransitionProgressProvider); + mUnfoldMoveFromCenterHotseatAnimator = new UnfoldMoveFromCenterHotseatAnimator(launcher, + windowManager, rotationChangeProvider); + mUnfoldMoveFromCenterWorkspaceAnimator = new UnfoldMoveFromCenterWorkspaceAnimator(launcher, + windowManager, rotationChangeProvider); mNaturalOrientationProgressProvider = new NaturalRotationUnfoldProgressProvider(launcher, - WindowManagerGlobal.getWindowManagerService(), mProgressProvider); + rotationChangeProvider, mProgressProvider); mNaturalOrientationProgressProvider.init(); // Animated in all orientations - mProgressProvider.addCallback(new UnfoldMoveFromCenterWorkspaceAnimator(launcher, - windowManager)); + mProgressProvider.addCallback(mUnfoldMoveFromCenterWorkspaceAnimator); + mProgressProvider.addCallback(new LauncherScaleAnimationListener()); // Animated only in natural orientation - mNaturalOrientationProgressProvider - .addCallback(new QsbAnimationListener()); - mNaturalOrientationProgressProvider - .addCallback(new UnfoldMoveFromCenterHotseatAnimator(launcher, windowManager)); + mNaturalOrientationProgressProvider.addCallback(new QsbAnimationListener()); + mNaturalOrientationProgressProvider.addCallback(mUnfoldMoveFromCenterHotseatAnimator); } /** @@ -79,8 +96,18 @@ public class LauncherUnfoldAnimationController { mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb(); } + handleTransitionOnNextFrame(); + } + + private void handleTransitionOnNextFrame() { + Trace.asyncTraceBegin(Trace.TRACE_TAG_APP, + TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0); OneShotPreDrawListener.add(mLauncher.getWorkspace(), - () -> mProgressProvider.setReadyToHandleTransition(true)); + () -> { + Trace.asyncTraceEnd(Trace.TRACE_TAG_APP, + TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0); + mProgressProvider.setReadyToHandleTransition(true); + }); } /** @@ -99,6 +126,12 @@ public class LauncherUnfoldAnimationController { mNaturalOrientationProgressProvider.destroy(); } + /** Called when launcher finished binding its items. */ + public void updateRegisteredViewsIfNeeded() { + mUnfoldMoveFromCenterHotseatAnimator.updateRegisteredViewsIfNeeded(); + mUnfoldMoveFromCenterWorkspaceAnimator.updateRegisteredViewsIfNeeded(); + } + private class QsbAnimationListener implements TransitionProgressListener { @Override @@ -115,9 +148,42 @@ public class LauncherUnfoldAnimationController { @Override public void onTransitionProgress(float progress) { if (mQsbInsettable != null) { - float insetPercentage = comp(progress) * MAX_WIDTH_INSET_FRACTION; + float insetPercentage = (1 - progress) * MAX_WIDTH_INSET_FRACTION; mQsbInsettable.setHorizontalInsets(insetPercentage); } } + + @Override + public void onTransitionFinishing() { + } + } + + private class LauncherScaleAnimationListener implements TransitionProgressListener { + + private static final float SCALE_LAUNCHER_FROM = 0.92f; + + @Override + public void onTransitionStarted() { + mLauncher.getWorkspace().setPivotToScaleWithSelf(mLauncher.getHotseat()); + } + + @Override + public void onTransitionFinished() { + setScale(1); + } + + @Override + public void onTransitionProgress(float progress) { + setScale(MathUtils.constrainedMap(SCALE_LAUNCHER_FROM, 1, 0, 1, progress)); + } + + private void setScale(float value) { + WORKSPACE_SCALE_PROPERTY.setValue(mLauncher.getWorkspace(), value); + HOTSEAT_SCALE_PROPERTY.setValue(mLauncher.getHotseat(), value); + } + + @Override + public void onTransitionFinishing() { + } } } diff --git a/quickstep/src/com/android/quickstep/util/LauncherViewsMoveFromCenterTranslationApplier.java b/quickstep/src/com/android/quickstep/util/LauncherViewsMoveFromCenterTranslationApplier.java index effdfdd97b..f6b2441054 100644 --- a/quickstep/src/com/android/quickstep/util/LauncherViewsMoveFromCenterTranslationApplier.java +++ b/quickstep/src/com/android/quickstep/util/LauncherViewsMoveFromCenterTranslationApplier.java @@ -15,12 +15,13 @@ */ package com.android.quickstep.util; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_MOVE_FROM_CENTER_ANIM; + import android.annotation.NonNull; import android.view.View; -import com.android.launcher3.BubbleTextView; -import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.widget.NavigableAppWidgetHostView; +import com.android.launcher3.Reorderable; +import com.android.launcher3.util.MultiTranslateDelegate; import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.TranslationApplier; /** @@ -31,12 +32,9 @@ public class LauncherViewsMoveFromCenterTranslationApplier implements Translatio @Override public void apply(@NonNull View view, float x, float y) { - if (view instanceof NavigableAppWidgetHostView) { - ((NavigableAppWidgetHostView) view).setTranslationForMoveFromCenterAnimation(x, y); - } else if (view instanceof BubbleTextView) { - ((BubbleTextView) view).setTranslationForMoveFromCenterAnimation(x, y); - } else if (view instanceof FolderIcon) { - ((FolderIcon) view).setTranslationForMoveFromCenterAnimation(x, y); + if (view instanceof Reorderable) { + MultiTranslateDelegate mtd = ((Reorderable) view).getTranslateDelegate(); + mtd.setTranslation(INDEX_MOVE_FROM_CENTER_ANIM, x, y); } else { view.setTranslationX(x); view.setTranslationY(y); diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java index 302526d07a..79656c27a4 100644 --- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java +++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java @@ -22,8 +22,9 @@ import android.view.ViewGroup; import com.android.launcher3.DeviceProfile; import com.android.launcher3.touch.PagedOrientationHandler; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.NavigationMode; import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.SysUINavigationMode; public class LayoutUtils { @@ -32,7 +33,7 @@ public class LayoutUtils { */ public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) { float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx; - if (SysUINavigationMode.getMode(context) == SysUINavigationMode.Mode.NO_BUTTON) { + if (DisplayController.getNavigationMode(context) == NavigationMode.NO_BUTTON) { swipeHeight -= dp.getInsets().bottom; } return swipeHeight; @@ -42,7 +43,8 @@ public class LayoutUtils { PagedOrientationHandler orientationHandler) { // Track the bottom of the window. Rect taskSize = new Rect(); - LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize); + LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize, + orientationHandler); return orientationHandler.getDistanceToBottomOfRect(dp, taskSize); } diff --git a/quickstep/src/com/android/quickstep/util/LogUtils.kt b/quickstep/src/com/android/quickstep/util/LogUtils.kt new file mode 100644 index 0000000000..23a41f6068 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/LogUtils.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import android.util.Pair +import com.android.internal.logging.InstanceIdSequence +import com.android.launcher3.logging.InstanceId + +object LogUtils { + /** + * @return a [Pair] of two InstanceIds but with different types, one that can be used by + * framework (if needing to pass through an intent or such) and one used in Launcher + */ + @JvmStatic + fun getShellShareableInstanceId(): Pair { + val internalInstanceId = InstanceIdSequence(InstanceId.INSTANCE_ID_MAX).newInstanceId() + val launcherInstanceId = InstanceId(internalInstanceId.id) + return Pair(internalInstanceId, launcherInstanceId) + } +} diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java index b83e26e5de..4bc41bc4ca 100644 --- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java +++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java @@ -17,13 +17,14 @@ package com.android.quickstep.util; import android.content.Context; import android.content.res.Resources; +import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import com.android.launcher3.Alarm; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.compat.AccessibilityManagerCompat; -import com.android.launcher3.testing.TestProtocol; /** * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is @@ -31,6 +32,8 @@ import com.android.launcher3.testing.TestProtocol; */ public class MotionPauseDetector { + private static final String TAG = "MotionPauseDetector"; + // The percentage of the previous speed that determines whether this is a rapid deceleration. // The bigger this number, the easier it is to trigger the first pause. private static final float RAPID_DECELERATION_FACTOR = 0.6f; @@ -43,6 +46,12 @@ public class MotionPauseDetector { */ private static final long HARDER_TRIGGER_TIMEOUT = 400; + /** + * When running in a test harness, if no motion is added for this amount of time, assume the + * motion has paused. (We use an increased timeout since sometimes test devices can be slow.) + */ + private static final long TEST_HARNESS_TRIGGER_TIMEOUT = 2000; + private final float mSpeedVerySlow; private final float mSpeedSlow; private final float mSpeedSomewhatFast; @@ -85,7 +94,8 @@ public class MotionPauseDetector { mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast); mForcePauseTimeout = new Alarm(); - mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */)); + mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */, + "Force pause timeout after " + alarm.getLastSetTimeout() + "ms" /* reason */)); mMakePauseHarderToTrigger = makePauseHarderToTrigger; mVelocityProvider = new SystemVelocityProvider(axis); } @@ -102,7 +112,7 @@ public class MotionPauseDetector { */ public void setDisallowPause(boolean disallowPause) { mDisallowPause = disallowPause; - updatePaused(mIsPaused); + updatePaused(mIsPaused, "Set disallowPause=" + disallowPause); } /** @@ -119,9 +129,11 @@ public class MotionPauseDetector { * @param pointerIndex Index for the pointer being tracked in the motion event */ public void addPosition(MotionEvent ev, int pointerIndex) { - long timeoutMs = TestProtocol.sForcePauseTimeout != null - ? TestProtocol.sForcePauseTimeout - : mMakePauseHarderToTrigger ? HARDER_TRIGGER_TIMEOUT : FORCE_PAUSE_TIMEOUT; + long timeoutMs = Utilities.isRunningInTestHarness() + ? TEST_HARNESS_TRIGGER_TIMEOUT + : mMakePauseHarderToTrigger + ? HARDER_TRIGGER_TIMEOUT + : FORCE_PAUSE_TIMEOUT; mForcePauseTimeout.setAlarm(timeoutMs); float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex)); if (mPreviousVelocity != null) { @@ -134,21 +146,27 @@ public class MotionPauseDetector { float speed = Math.abs(velocity); float previousSpeed = Math.abs(prevVelocity); boolean isPaused; + String isPausedReason = ""; if (mIsPaused) { // Continue to be paused until moving at a fast speed. isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast; + isPausedReason = "Was paused, but started moving at a fast speed"; } else { if (velocity < 0 != prevVelocity < 0) { // We're just changing directions, not necessarily stopping. isPaused = false; + isPausedReason = "Velocity changed directions"; } else { isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow; + isPausedReason = "Pause requires back to back slow speeds"; if (!isPaused && !mHasEverBeenPaused) { // 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; isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; + isPausedReason = "Didn't have back to back slow speeds, checking for rapid" + + " deceleration on first pause only"; } if (mMakePauseHarderToTrigger) { if (speed < mSpeedSlow) { @@ -156,22 +174,31 @@ public class MotionPauseDetector { mSlowStartTime = time; } isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT; + isPausedReason = "Maintained slow speed for sufficient duration when making" + + " pause harder to trigger"; } else { mSlowStartTime = 0; isPaused = false; + isPausedReason = "Intentionally making pause harder to trigger"; } } } } - updatePaused(isPaused); + updatePaused(isPaused, isPausedReason); } - private void updatePaused(boolean isPaused) { + private void updatePaused(boolean isPaused, String reason) { if (mDisallowPause) { + reason = "Disallow pause; otherwise, would have been " + isPaused + " due to " + reason; isPaused = false; } if (mIsPaused != isPaused) { mIsPaused = isPaused; + String logString = "onMotionPauseChanged, paused=" + mIsPaused + " reason=" + reason; + if (Utilities.isRunningInTestHarness()) { + Log.d(TAG, logString); + } + ActiveGestureLog.INSTANCE.addLog(logString); boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused; if (mIsPaused) { AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext); diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java index 449dba8f26..59c82633d2 100644 --- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java +++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java @@ -15,27 +15,27 @@ */ package com.android.quickstep.util; -import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; +import static com.android.launcher3.util.NavigationMode.NO_BUTTON; import android.view.Surface; import com.android.launcher3.util.DisplayController.Info; -import com.android.quickstep.SysUINavigationMode; +import com.android.launcher3.util.NavigationMode; /** * Utility class to check nav bar position. */ public class NavBarPosition { - private final SysUINavigationMode.Mode mMode; + private final NavigationMode mMode; private final int mDisplayRotation; - public NavBarPosition(SysUINavigationMode.Mode mode, Info info) { + public NavBarPosition(NavigationMode mode, Info info) { mMode = mode; mDisplayRotation = info.rotation; } - public NavBarPosition(SysUINavigationMode.Mode mode, int displayRotation) { + public NavBarPosition(NavigationMode mode, int displayRotation) { mMode = mode; mDisplayRotation = displayRotation; } diff --git a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java index 5cf4f0b97f..3cec1a4f1a 100644 --- a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java +++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java @@ -82,7 +82,7 @@ public class OverviewToHomeAnim { // WorkspaceRevealAnim handles the depth, so don't interfere. config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER; } - config.duration = startState.getTransitionDuration(mLauncher); + config.duration = startState.getTransitionDuration(mLauncher, false /* isToState */); AnimatorSet stateAnim = stateManager.createAtomicAnimation( startState, NORMAL, config); stateAnim.addListener(new AnimationSuccessListener() { diff --git a/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java new file mode 100644 index 0000000000..e189a66ee4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; +import static com.android.launcher3.anim.Interpolators.INSTANT; + +import android.view.animation.Interpolator; + +/** + * Timings for the Overview > OverviewSplitSelect animation. + */ +abstract class OverviewToSplitTimings implements SplitAnimationTimings { + // Overwritten by device-specific timings + abstract public int getPlaceholderFadeInStart(); + abstract public int getPlaceholderFadeInEnd(); + abstract public int getPlaceholderIconFadeInStart(); + abstract public int getPlaceholderIconFadeInEnd(); + abstract public int getStagedRectSlideStart(); + abstract public int getStagedRectSlideEnd(); + abstract public int getGridSlideStart(); + abstract public int getGridSlideStagger(); + abstract public int getGridSlideDuration(); + + // Common timings + public int getIconFadeStart() { return 0; } + public int getIconFadeEnd() { return 83; } + public int getActionsFadeStart() { return 0; } + public int getActionsFadeEnd() { return 83; } + public int getInstructionsContainerFadeInStart() { return 167; } + public int getInstructionsContainerFadeInEnd() { return 250; } + public int getInstructionsTextFadeInStart() { return 217; } + public int getInstructionsTextFadeInEnd() { return 300; } + public int getInstructionsUnfoldStart() { return 167; } + public int getInstructionsUnfoldEnd() { return 500; } + public Interpolator getGridSlidePrimaryInterpolator() { return EMPHASIZED; } + public Interpolator getGridSlideSecondaryInterpolator() { return INSTANT; } + + abstract public int getDuration(); + abstract public Interpolator getStagedRectXInterpolator(); + abstract public Interpolator getStagedRectYInterpolator(); + abstract public Interpolator getStagedRectScaleXInterpolator(); + abstract public Interpolator getStagedRectScaleYInterpolator(); + + public float getGridSlideStartOffset() { + return (float) getGridSlideStart() / getDuration(); + } + public float getGridSlideStaggerOffset() { + return (float) getGridSlideStagger() / getDuration(); + } + public float getGridSlideDurationOffset() { + return (float) getGridSlideDuration() / getDuration(); + } + public float getActionsFadeStartOffset() { + return (float) getActionsFadeStart() / getDuration(); + } + public float getActionsFadeEndOffset() { + return (float) getActionsFadeEnd() / getDuration(); + } + public float getIconFadeStartOffset() { + return (float) getIconFadeStart() / getDuration(); + } + public float getIconFadeEndOffset() { + return (float) getIconFadeEnd() / getDuration(); + } + public float getInstructionsContainerFadeInStartOffset() { + return (float) getInstructionsContainerFadeInStart() / getDuration(); + } + public float getInstructionsContainerFadeInEndOffset() { + return (float) getInstructionsContainerFadeInEnd() / getDuration(); + } + public float getInstructionsTextFadeInStartOffset() { + return (float) getInstructionsTextFadeInStart() / getDuration(); + } + public float getInstructionsTextFadeInEndOffset() { + return (float) getInstructionsTextFadeInEnd() / getDuration(); + } + public float getInstructionsUnfoldStartOffset() { + return (float) getInstructionsUnfoldStart() / getDuration(); + } + public float getInstructionsUnfoldEndOffset() { + return (float) getInstructionsUnfoldEnd() / getDuration(); + } +} diff --git a/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java new file mode 100644 index 0000000000..f1dde53d12 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; + +import android.view.animation.Interpolator; + +/** + * Timings for the Overview > OverviewSplitSelect animation on phones. + */ +public class PhoneOverviewToSplitTimings + extends OverviewToSplitTimings implements SplitAnimationTimings { + public int getPlaceholderFadeInStart() { return 0; } + public int getPlaceholderFadeInEnd() { return 133; } + public int getPlaceholderIconFadeInStart() { return 83; } + public int getPlaceholderIconFadeInEnd() { return 167; } + public int getStagedRectSlideStart() { return 0; } + public int getStagedRectSlideEnd() { return 333; } + public int getGridSlideStart() { return 100; } + public int getGridSlideStagger() { return 0; } + public int getGridSlideDuration() { return 417; } + + public int getDuration() { return PHONE_ENTER_DURATION; } + public Interpolator getStagedRectXInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectYInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectScaleXInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectScaleYInterpolator() { return EMPHASIZED; } +} diff --git a/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java new file mode 100644 index 0000000000..3d9e09ee81 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +/** + * Timings for the OverviewSplitSelect > confirmed animation on phones. + */ +public class PhoneSplitToConfirmTimings + extends SplitToConfirmTimings implements SplitAnimationTimings { + public int getPlaceholderFadeInStart() { return 0; } + public int getPlaceholderFadeInEnd() { return 133; } + public int getPlaceholderIconFadeInStart() { return 50; } + public int getPlaceholderIconFadeInEnd() { return 133; } + public int getStagedRectSlideStart() { return 0; } + public int getStagedRectSlideEnd() { return 333; } + + public int getDuration() { return PHONE_CONFIRM_DURATION; } +} diff --git a/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java b/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java index 3777c659e3..8f79ccf450 100644 --- a/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java +++ b/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java @@ -39,6 +39,16 @@ public class ProxyScreenStatusProvider implements ScreenStatusProvider { mListeners.forEach(ScreenListener::onScreenTurnedOn); } + /** Called when the screen is starting to turn on. */ + public void onScreenTurningOn() { + mListeners.forEach(ScreenListener::onScreenTurningOn); + } + + /** Called when the screen is starting to turn off. */ + public void onScreenTurningOff() { + mListeners.forEach(ScreenListener::onScreenTurningOff); + } + @Override public void addCallback(@NonNull ScreenListener listener) { mListeners.add(listener); diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java index 6d6e802231..cf07e6e687 100644 --- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java +++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java @@ -21,20 +21,20 @@ import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; -import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; +import static com.android.launcher3.util.NavigationMode.NO_BUTTON; import android.content.SharedPreferences; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; -import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.appprediction.AppsDividerView; import com.android.launcher3.hybridhotseat.HotseatPredictionController; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager.StateListener; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.OnboardingPrefs; -import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.views.AllAppsEduView; /** @@ -50,8 +50,8 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs stateManager.addStateListener(new StateListener() { @Override public void onStateTransitionComplete(LauncherState finalState) { - boolean swipeUpEnabled = SysUINavigationMode.INSTANCE - .get(mLauncher).getMode().hasGestures; + boolean swipeUpEnabled = + DisplayController.getNavigationMode(mLauncher).hasGestures; LauncherState prevState = stateManager.getLastState(); if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled @@ -64,7 +64,7 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs }); } - if (!Utilities.IS_RUNNING_IN_TEST_HARNESS + if (!Utilities.isRunningInTestHarness() && !hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) { stateManager.addStateListener(new StateListener() { boolean mFromAllApps = false; @@ -87,8 +87,7 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs }); } - if (SysUINavigationMode.getMode(launcher) == NO_BUTTON - && FeatureFlags.ENABLE_ALL_APPS_EDU.get()) { + if (DisplayController.getNavigationMode(launcher) == NO_BUTTON) { stateManager.addStateListener(new StateListener() { private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3; @@ -132,5 +131,24 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs } }); } + + if (!hasReachedMaxCount(ALL_APPS_VISITED_COUNT)) { + mLauncher.getStateManager().addStateListener(new StateListener() { + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if (finalState == ALL_APPS) { + incrementEventCount(ALL_APPS_VISITED_COUNT); + return; + } + + boolean hasReachedMaxCount = hasReachedMaxCount(ALL_APPS_VISITED_COUNT); + mLauncher.getAppsView().getFloatingHeaderView().findFixedRowByType( + AppsDividerView.class).setShowAllAppsLabel(!hasReachedMaxCount); + if (hasReachedMaxCount) { + mLauncher.getStateManager().removeStateListener(this); + } + } + }); + } } } diff --git a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java index edaa32637a..1d008da5b3 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java +++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java @@ -15,13 +15,10 @@ */ package com.android.quickstep.util; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; -import android.util.Log; import androidx.dynamicanimation.animation.DynamicAnimation; @@ -30,8 +27,6 @@ import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory; import com.android.launcher3.statemanager.StatefulActivity; import com.android.quickstep.views.RecentsView; -import java.util.Arrays; - public class RecentsAtomicAnimationFactory extends AtomicAnimationFactory { @@ -53,27 +48,6 @@ public class RecentsAtomicAnimationFactory 0) { - scale = scale * dp.widthPx / fullWidth; - } - if (scale == 1) { - outPivot.set(fullWidth / 2, fullHeight / 2); - } else if (dp.isMultiWindowMode) { - float denominator = 1 / (scale - 1); - // Ensure that the task aligns to right bottom for the root view - float y = (scale * taskView.bottom - fullHeight) * denominator; - float x = (scale * taskView.right - fullWidth) * denominator; - outPivot.set(x, y); + outPivot.set(taskView.centerX(), taskView.centerY()); } else { float factor = scale / (scale - 1); outPivot.set(taskView.left * factor, taskView.top * factor); @@ -593,7 +576,7 @@ public class RecentsOrientedState implements width = Math.min(currentSize.x, currentSize.y); height = Math.max(currentSize.x, currentSize.y); } - return idp.getBestMatch(width, height); + return idp.getBestMatch(width, height, mRecentsActivityRotation); } private static String nameAndAddress(Object obj) { diff --git a/quickstep/src/com/android/quickstep/util/RecordingSurfaceTransaction.java b/quickstep/src/com/android/quickstep/util/RecordingSurfaceTransaction.java new file mode 100644 index 0000000000..a2f48ddc9b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/RecordingSurfaceTransaction.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +/** + * Extension for {@link SurfaceTransaction} which records the commands for mocking + */ +public class RecordingSurfaceTransaction extends SurfaceTransaction { + + /** + * A mock builder which can be used for recording values + */ + public final MockProperties mockProperties = new MockProperties(); + +} diff --git a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java index c4909de510..251b7567b1 100644 --- a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java +++ b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java @@ -128,37 +128,27 @@ public class RectFSpringAnim extends ReleaseCheck { @Tracking public final int mTracking; + protected final float mStiffnessX; + protected final float mStiffnessY; + protected final float mDampingX; + protected final float mDampingY; + protected final float mRectStiffness; - public RectFSpringAnim(RectF startRect, RectF targetRect, Context context, - @Nullable DeviceProfile deviceProfile) { - mStartRect = startRect; - mTargetRect = targetRect; + public RectFSpringAnim(SpringConfig config) { + mStartRect = config.startRect; + mTargetRect = config.targetRect; mCurrentCenterX = mStartRect.centerX(); - ResourceProvider rp = DynamicResource.provider(context); - mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change); - mMaxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity); + mMinVisChange = config.minVisChange; + mMaxVelocityPxPerS = config.maxVelocityPxPerS; setCanRelease(true); - if (deviceProfile == null) { - mTracking = startRect.bottom < targetRect.bottom - ? TRACKING_BOTTOM - : TRACKING_TOP; - } else { - int heightPx = deviceProfile.heightPx; - Rect padding = deviceProfile.workspacePadding; - - final float topThreshold = heightPx / 3f; - final float bottomThreshold = deviceProfile.heightPx - padding.bottom; - - if (targetRect.bottom > bottomThreshold) { - mTracking = TRACKING_BOTTOM; - } else if (targetRect.top < topThreshold) { - mTracking = TRACKING_TOP; - } else { - mTracking = TRACKING_CENTER; - } - } + mTracking = config.tracking; + mStiffnessX = config.stiffnessX; + mStiffnessY = config.stiffnessY; + mDampingX = config.dampingX; + mDampingY = config.dampingY; + mRectStiffness = config.rectStiffness; mCurrentY = getTrackedYFromRect(mStartRect); } @@ -214,7 +204,7 @@ public class RectFSpringAnim extends ReleaseCheck { * @param context The activity context. * @param velocityPxPerMs Velocity of swipe in px/ms. */ - public void start(Context context, PointF velocityPxPerMs) { + public void start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs) { // Only tell caller that we ended if both x and y animations have ended. OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> { mRectXAnimEnded = true; @@ -240,19 +230,26 @@ public class RectFSpringAnim extends ReleaseCheck { float maxXValue = Math.max(startX, endX); mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX, - dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, onXEndListener); + dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, mDampingX, mStiffnessX, + onXEndListener); float startY = mCurrentY; float endY = getTrackedYFromRect(mTargetRect); float minYValue = Math.min(startY, endY); float maxYValue = Math.max(startY, endY); mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, dampedYVelocityPxPerS, - mMinVisChange, minYValue, maxYValue, onYEndListener); + mMinVisChange, minYValue, maxYValue, mDampingY, mStiffnessY, onYEndListener); float minVisibleChange = Math.abs(1f / mStartRect.height()); ResourceProvider rp = DynamicResource.provider(context); float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio); - float stiffness = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness); + + // Increase the stiffness for devices where we want the window size to transform quicker. + boolean shouldUseHigherStiffness = profile != null + && (profile.isLandscape || profile.isTablet); + float stiffness = shouldUseHigherStiffness + ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness) + : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness); mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS) .setSpring(new SpringForce(1f) @@ -362,4 +359,98 @@ public class RectFSpringAnim extends ReleaseCheck { default void onCancel() { } } + + private abstract static class SpringConfig { + protected RectF startRect; + protected RectF targetRect; + protected @Tracking int tracking; + protected float stiffnessX; + protected float stiffnessY; + protected float dampingX; + protected float dampingY; + protected float rectStiffness; + protected float minVisChange; + protected int maxVelocityPxPerS; + + private SpringConfig(Context context, RectF start, RectF target) { + startRect = start; + targetRect = target; + + ResourceProvider rp = DynamicResource.provider(context); + minVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change); + maxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity); + } + } + + /** + * Standard spring configuration parameters. + */ + public static class DefaultSpringConfig extends SpringConfig { + + public DefaultSpringConfig(Context context, DeviceProfile deviceProfile, + RectF startRect, RectF targetRect) { + super(context, startRect, targetRect); + + ResourceProvider rp = DynamicResource.provider(context); + tracking = getDefaultTracking(deviceProfile); + stiffnessX = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness); + stiffnessY = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness); + dampingX = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio); + dampingY = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio); + + this.startRect = startRect; + this.targetRect = targetRect; + + // Increase the stiffness for devices where we want the window size to transform + // quicker. + boolean shouldUseHigherStiffness = deviceProfile != null + && (deviceProfile.isLandscape || deviceProfile.isTablet); + rectStiffness = shouldUseHigherStiffness + ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness) + : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness); + } + + private @Tracking int getDefaultTracking(@Nullable DeviceProfile deviceProfile) { + @Tracking int tracking; + if (deviceProfile == null) { + tracking = startRect.bottom < targetRect.bottom + ? TRACKING_BOTTOM + : TRACKING_TOP; + } else { + int heightPx = deviceProfile.heightPx; + Rect padding = deviceProfile.workspacePadding; + + final float topThreshold = heightPx / 3f; + final float bottomThreshold = deviceProfile.heightPx - padding.bottom; + + if (targetRect.bottom > bottomThreshold) { + tracking = TRACKING_BOTTOM; + } else if (targetRect.top < topThreshold) { + tracking = TRACKING_TOP; + } else { + tracking = TRACKING_CENTER; + } + } + return tracking; + } + } + + /** + * Spring configuration parameters for Taskbar/Hotseat items on devices that have a taskbar. + */ + public static class TaskbarHotseatSpringConfig extends SpringConfig { + + public TaskbarHotseatSpringConfig(Context context, RectF start, RectF target) { + super(context, start, target); + + ResourceProvider rp = DynamicResource.provider(context); + tracking = TRACKING_CENTER; + stiffnessX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_stiffness); + stiffnessY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_stiffness); + dampingX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_damping); + dampingY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_damping); + rectStiffness = rp.getFloat(R.dimen.taskbar_swipe_up_rect_scale_stiffness); + } + } + } diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java index ee82ae67df..10f2eaa2f0 100644 --- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java +++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java @@ -16,23 +16,22 @@ package com.android.quickstep.util; import android.animation.AnimatorSet; - -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import android.view.RemoteAnimationTarget; public abstract class RemoteAnimationProvider { - public abstract AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets); + public abstract AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets); /** * @return the target with the lowest opaque layer for a certain app animation, or null. */ - public static RemoteAnimationTargetCompat findLowestOpaqueLayerTarget( - RemoteAnimationTargetCompat[] appTargets, int mode) { + public static RemoteAnimationTarget findLowestOpaqueLayerTarget( + RemoteAnimationTarget[] appTargets, int mode) { int lowestLayer = Integer.MAX_VALUE; int lowestLayerIndex = -1; for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTargetCompat target = appTargets[i]; + RemoteAnimationTarget target = appTargets[i]; if (target.mode == mode && !target.isTranslucent) { int layer = target.prefixOrderIndex; if (layer < lowestLayer) { diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java index 81c124f7e2..382cf79e1e 100644 --- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java +++ b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java @@ -15,14 +15,14 @@ */ package com.android.quickstep.util; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl.Transaction; import com.android.quickstep.RemoteAnimationTargets; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.TransactionCompat; /** * Animation listener which fades out the closing targets @@ -32,24 +32,24 @@ public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener { private final RemoteAnimationTargets mTarget; private boolean mFirstFrame = true; - public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] appTargets, - RemoteAnimationTargetCompat[] wallpaperTargets) { + public RemoteFadeOutAnimationListener(RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets) { mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets, - new RemoteAnimationTargetCompat[0], MODE_CLOSING); + new RemoteAnimationTarget[0], MODE_CLOSING); } @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { - TransactionCompat t = new TransactionCompat(); + Transaction t = new Transaction(); if (mFirstFrame) { - for (RemoteAnimationTargetCompat target : mTarget.unfilteredApps) { + for (RemoteAnimationTarget target : mTarget.unfilteredApps) { t.show(target.leash); } mFirstFrame = false; } float alpha = 1 - valueAnimator.getAnimatedFraction(); - for (RemoteAnimationTargetCompat app : mTarget.apps) { + for (RemoteAnimationTarget app : mTarget.apps) { t.setAlpha(app.leash, alpha); } t.apply(); diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt new file mode 100644 index 0000000000..b76fe5cb03 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -0,0 +1,179 @@ +/* + * 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 android.animation.ObjectAnimator +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.View +import com.android.launcher3.DeviceProfile +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource +import com.android.quickstep.views.IconView +import com.android.quickstep.views.TaskThumbnailView +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer +import java.util.function.Supplier + +/** + * Utils class to help run animations for initiating split screen from launcher. + * Will be expanded with future refactors. Works in conjunction with the state stored in + * [SplitSelectStateController] + */ +class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { + companion object { + // Break this out into maybe enums? Abstractions into its own classes? Tbd. + data class SplitAnimInitProps( + val originalView: View, + val originalBitmap: Bitmap?, + val iconDrawable: Drawable, + val fadeWithThumbnail: Boolean, + val isStagedTask: Boolean, + val iconView: View? + ) + } + + /** + * Returns different elements to animate for the initial split selection animation + * depending on the state of the surface from which the split was initiated + */ + fun getFirstAnimInitViews(taskViewSupplier: Supplier, + splitSelectSourceSupplier: Supplier) + : SplitAnimInitProps { + val splitSelectSource = splitSelectSourceSupplier.get() + if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { + // Initiating from home + return SplitAnimInitProps(splitSelectSource!!.view, originalBitmap = null, + splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, + iconView = null) + } else if (splitSelectStateController.isDismissingFromSplitPair) { + // Initiating split from overview, but on a split pair + val taskView = taskViewSupplier.get() + for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { + if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { + val drawable = getDrawable(container.iconView, splitSelectSource) + return SplitAnimInitProps(container.thumbnailView, + container.thumbnailView.thumbnail, drawable!!, + fadeWithThumbnail = true, isStagedTask = true, + iconView = container.iconView + ) + } + } + throw IllegalStateException("Attempting to init split from existing split pair " + + "without a valid taskIdAttributeContainer") + } else { + // Initiating split from overview on fullscreen task TaskView + val taskView = taskViewSupplier.get() + val drawable = getDrawable(taskView.iconView, splitSelectSource) + return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, + drawable!!, fadeWithThumbnail = true, isStagedTask = true, + taskView.iconView + ) + } + } + + /** + * 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 + * @return [Drawable] + */ + fun getDrawable(iconView: IconView, splitSelectSource: SplitSelectSource?) : Drawable? { + if (iconView.drawable == null && splitSelectSource != null) { + return splitSelectSource.drawable + } + return iconView.drawable + } + + /** + * 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 [TaskThumbnailView]'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 isPrimaryTaskSplitting if true, task that was split would be top/left in the pair + * (opposite of that representing [taskIdAttributeContainer]) + */ + fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer, + builder: PendingAnimation, deviceProfile: DeviceProfile, + taskViewWidth: Int, taskViewHeight: Int, + isPrimaryTaskSplitting: Boolean) { + val thumbnail = taskIdAttributeContainer.thumbnailView + val iconView: View = taskIdAttributeContainer.iconView + builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f)) + thumbnail.setShowSplashForSplitSelection(true) + if (deviceProfile.isLandscape) { + // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 + val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f + val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f + val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX)) + // icons are anchored from Gravity.END, so need to use negative translation + builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, + -centerIconTranslationX)) + builder.add(ObjectAnimator.ofFloat(thumbnail, 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 + val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else + deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, + translateYResetVal)) + } else { + val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx + // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 + // primary thumbnail has layout margin above it, so secondary thumbnail needs to take + // that into account. We should migrate to only using translations otherwise this + // asymmetry causes problems.. + + // Icon defaults to center | horizontal, we add additional translation for split + val centerIconTranslationX = 0f + var centerThumbnailTranslationY: Float + + // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary + // 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 += deviceProfile.overviewTaskThumbnailTopMarginPx + .toFloat() + } else { + centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + } + val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY)) + + // icons are anchored from Gravity.END, so need to use negative translation + builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, + centerIconTranslationX)) + builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) + + // Reset other dimensions + thumbnail.scaleX = 1f + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java new file mode 100644 index 0000000000..7dc1b32858 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.LINEAR; + +import android.view.animation.Interpolator; + +/** + * An interface that supports the centralization of timing information for splitscreen animations. + */ +public interface SplitAnimationTimings { + int TABLET_ENTER_DURATION = 866; + int TABLET_CONFIRM_DURATION = 500; + + int PHONE_ENTER_DURATION = 517; + int PHONE_CONFIRM_DURATION = 333; + + int ABORT_DURATION = 500; + + SplitAnimationTimings TABLET_OVERVIEW_TO_SPLIT = new TabletOverviewToSplitTimings(); + SplitAnimationTimings TABLET_HOME_TO_SPLIT = new TabletHomeToSplitTimings(); + SplitAnimationTimings TABLET_SPLIT_TO_CONFIRM = new TabletSplitToConfirmTimings(); + + SplitAnimationTimings PHONE_OVERVIEW_TO_SPLIT = new PhoneOverviewToSplitTimings(); + SplitAnimationTimings PHONE_SPLIT_TO_CONFIRM = new PhoneSplitToConfirmTimings(); + + // Shared methods + int getDuration(); + int getPlaceholderFadeInStart(); + int getPlaceholderFadeInEnd(); + int getPlaceholderIconFadeInStart(); + int getPlaceholderIconFadeInEnd(); + int getStagedRectSlideStart(); + int getStagedRectSlideEnd(); + Interpolator getStagedRectXInterpolator(); + Interpolator getStagedRectYInterpolator(); + Interpolator getStagedRectScaleXInterpolator(); + Interpolator getStagedRectScaleYInterpolator(); + default float getPlaceholderFadeInStartOffset() { + return (float) getPlaceholderFadeInStart() / getDuration(); + } + default float getPlaceholderFadeInEndOffset() { + return (float) getPlaceholderFadeInEnd() / getDuration(); + } + default float getPlaceholderIconFadeInStartOffset() { + return (float) getPlaceholderIconFadeInStart() / getDuration(); + } + default float getPlaceholderIconFadeInEndOffset() { + return (float) getPlaceholderIconFadeInEnd() / getDuration(); + } + default float getStagedRectSlideStartOffset() { + return (float) getStagedRectSlideStart() / getDuration(); + } + default float getStagedRectSlideEndOffset() { + return (float) getStagedRectSlideEnd() / getDuration(); + } + + // Defaults for OverviewToSplit + default float getGridSlideStartOffset() { return 0; } + default float getGridSlideStaggerOffset() { return 0; } + default float getGridSlideDurationOffset() { return 0; } + default float getActionsFadeStartOffset() { return 0; } + default float getActionsFadeEndOffset() { return 0; } + default float getIconFadeStartOffset() { return 0; } + default float getIconFadeEndOffset() { return 0; } + default float getInstructionsContainerFadeInStartOffset() { return 0; } + default float getInstructionsContainerFadeInEndOffset() { return 0; } + default float getInstructionsTextFadeInStartOffset() { return 0; } + default float getInstructionsTextFadeInEndOffset() { return 0; } + default float getInstructionsUnfoldStartOffset() { return 0; } + default float getInstructionsUnfoldEndOffset() { return 0; } + default Interpolator getGridSlidePrimaryInterpolator() { return LINEAR; } + default Interpolator getGridSlideSecondaryInterpolator() { return LINEAR; } + + // Defaults for HomeToSplit + default float getScrimFadeInStartOffset() { return 0; } + default float getScrimFadeInEndOffset() { return 0; } + + // Defaults for SplitToConfirm + default float getInstructionsFadeStartOffset() { return 0; } + default float getInstructionsFadeEndOffset() { return 0; } +} + diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java deleted file mode 100644 index 175fa6abf5..0000000000 --- a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java +++ /dev/null @@ -1,123 +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.quickstep.util; - -import static android.view.Surface.ROTATION_0; -import static android.view.Surface.ROTATION_180; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Build; -import android.view.Display; -import android.view.WindowManager; -import android.view.WindowMetrics; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; - -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.WindowBounds; - -import java.util.ArrayList; - -/** - * Utility class to hold the information abound a window bounds for split screen - */ -@TargetApi(Build.VERSION_CODES.R) -public class SplitScreenBounds { - - public static final SplitScreenBounds INSTANCE = new SplitScreenBounds(); - private final ArrayList mListeners = new ArrayList<>(); - - @Nullable - private WindowBounds mBounds; - - private SplitScreenBounds() { } - - @UiThread - public void setSecondaryWindowBounds(@NonNull WindowBounds bounds) { - if (!bounds.equals(mBounds)) { - mBounds = bounds; - for (OnChangeListener listener : mListeners) { - listener.onSecondaryWindowBoundsChanged(); - } - } - } - - public @NonNull WindowBounds getSecondaryWindowBounds(Context context) { - if (mBounds == null) { - mBounds = createDefaultWindowBounds(context); - } - return mBounds; - } - - /** - * Creates window bounds as 50% of device size - */ - private static WindowBounds createDefaultWindowBounds(Context context) { - WindowBounds bounds = getFullSizeWindowBounds(context); - - int rotation = DisplayController.INSTANCE.get(context).getInfo().rotation; - int halfDividerSize = context.getResources() - .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; - - if (rotation == ROTATION_0 || rotation == ROTATION_180) { - bounds.bounds.top = bounds.insets.top + bounds.availableSize.y / 2 + halfDividerSize; - bounds.insets.top = 0; - } else { - bounds.bounds.left = bounds.insets.left + bounds.availableSize.x / 2 + halfDividerSize; - bounds.insets.left = 0; - } - return new WindowBounds(bounds.bounds, bounds.insets); - } - - private static WindowBounds getFullSizeWindowBounds(Context context) { - WindowManager windowManager = context.getSystemService(WindowManager.class); - if (Utilities.ATLEAST_R) { - WindowMetrics wm = windowManager.getMaximumWindowMetrics(); - return WindowBounds.fromWindowMetrics(wm); - } else { - Display display = windowManager.getDefaultDisplay(); - Point mwSize = new Point(); - display.getSize(mwSize); - return new WindowBounds(new Rect(0, 0, mwSize.x, mwSize.y), new Rect()); - } - } - - public void addOnChangeListener(OnChangeListener listener) { - mListeners.add(listener); - } - - public void removeOnChangeListener(OnChangeListener listener) { - mListeners.remove(listener); - } - - /** - * Interface to receive window bounds changes - */ - public interface OnChangeListener { - - /** - * Called when window bounds for secondary window changes - */ - void onSecondaryWindowBoundsChanged(); - } -} diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index 5253e8c657..5a883bf4eb 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -16,38 +16,59 @@ package com.android.quickstep.util; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.PendingIntent.FLAG_MUTABLE; + import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; -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.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition; +import android.annotation.NonNull; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; -import android.graphics.Rect; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; +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.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionInfo; import androidx.annotation.Nullable; +import com.android.internal.logging.InstanceId; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; +import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.TaskViewUtils; +import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.RemoteTransitionCompat; -import com.android.systemui.shared.system.RemoteTransitionRunner; import java.util.function.Consumer; @@ -56,148 +77,432 @@ import java.util.function.Consumer; * and is in the process of either a) selecting a second app or b) exiting intention to invoke split */ public class SplitSelectStateController { + private static final String TAG = "SplitSelectStateCtor"; + private final Context mContext; private final Handler mHandler; + private final RecentsModel mRecentTasksModel; + private final SplitAnimationController mSplitAnimationController; + private StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; - private final DepthController mDepthController; - private @StagePosition int mStagePosition; - private Task mInitialTask; - private Task mSecondTask; + @Nullable + private DepthController mDepthController; + private @StagePosition int mInitialStagePosition; + private ItemInfo mItemInfo; + /** {@link #mInitialTaskIntent} and {@link #mInitialUser} (the user of the Intent) are set + * together when split is initiated from an Intent. */ + private Intent mInitialTaskIntent; + private UserHandle mInitialUser; + private int mInitialTaskId = INVALID_TASK_ID; + /** {@link #mSecondTaskIntent} and {@link #mSecondUser} (the user of the Intent) are set + * together when split is confirmed with an Intent. */ + private Intent mSecondTaskIntent; + private UserHandle mSecondUser; + private int mSecondTaskId = INVALID_TASK_ID; private boolean mRecentsAnimationRunning; + /** If {@code true}, animates the existing task view split placeholder view */ + private boolean mAnimateCurrentTaskDismissal; + /** + * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a + * split pair task view without wanting to animate current task dismissal overall + */ + private boolean mDismissingFromSplitPair; /** If not null, this is the TaskView we want to launch from */ @Nullable private GroupedTaskView mLaunchingTaskView; + /** Represents where split is intended to be invoked from. */ + private StatsLogManager.EventEnum mSplitEvent; - public SplitSelectStateController(Handler handler, SystemUiProxy systemUiProxy, - StateManager stateManager, - DepthController depthController) { + private FloatingTaskView mFirstFloatingTaskView; + + public SplitSelectStateController(Context context, Handler handler, StateManager stateManager, + DepthController depthController, StatsLogManager statsLogManager, + SystemUiProxy systemUiProxy, RecentsModel recentsModel) { + mContext = context; mHandler = handler; + mStatsLogManager = statsLogManager; mSystemUiProxy = systemUiProxy; mStateManager = stateManager; mDepthController = depthController; + mRecentTasksModel = recentsModel; + mSplitAnimationController = new SplitAnimationController(this); } /** - * To be called after first task selected + * @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID} + * then @param intent will be used to launch the initial task + * @param intent will be ignored if @param alreadyRunningTask is set */ - public void setInitialTaskSelect(Task task, @StagePosition int stagePosition, - Rect initialBounds) { - mInitialTask = task; - mStagePosition = stagePosition; + public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition, + @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, + int alreadyRunningTask) { + if (alreadyRunningTask != INVALID_TASK_ID) { + mInitialTaskId = alreadyRunningTask; + } else { + mInitialTaskIntent = intent; + mInitialUser = itemInfo.user; + } + + setInitialData(stagePosition, splitEvent, itemInfo); } /** - * To be called after second task selected + * To be called after first task selected from using a split shortcut from the fullscreen + * running app. */ - public void setSecondTaskId(Task task, Consumer callback) { - mSecondTask = task; - launchTasks(mInitialTask, mSecondTask, mStagePosition, callback, - false /* freezeTaskList */, DEFAULT_SPLIT_RATIO); + public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info, + @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, + StatsLogManager.EventEnum splitEvent) { + mInitialTaskId = info.taskId; + setInitialData(stagePosition, splitEvent, itemInfo); + } + + private void setInitialData(@StagePosition int stagePosition, + StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) { + mItemInfo = itemInfo; + mInitialStagePosition = stagePosition; + mSplitEvent = splitEvent; + } + + /** + * Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task + * matching a given ComponentName. Then uses that Task (which could be null) with the given + * callback. + * + * Used in various task-switching or splitscreen operations when we need to check if there is a + * currently running Task of a certain type and use the most recent one. + */ + public void findLastActiveTaskAndRunCallback(ComponentKey componentKey, + Consumer callback) { + mRecentTasksModel.getTasks(taskGroups -> { + Task lastActiveTask = null; + // Loop through tasks in reverse, since they are ordered with most-recent tasks last. + for (int i = taskGroups.size() - 1; i >= 0; i--) { + GroupTask groupTask = taskGroups.get(i); + Task task1 = groupTask.task1; + if (isInstanceOfComponent(task1, componentKey)) { + lastActiveTask = task1; + break; + } + Task task2 = groupTask.task2; + if (isInstanceOfComponent(task2, componentKey)) { + lastActiveTask = task2; + break; + } + } + + callback.accept(lastActiveTask); + }); + } + + /** + * Checks if a given Task is the most recently-active Task of type componentName. Used for + * selecting already-running Tasks for splitscreen. + */ + public boolean isInstanceOfComponent(@Nullable Task task, ComponentKey componentKey) { + // Exclude the task that is already staged + if (task == null || task.key.id == mInitialTaskId) { + return false; + } + + return task.key.baseIntent.getComponent().equals(componentKey.componentName) + && task.key.userId == componentKey.user.getIdentifier(); + } + + /** + * To be called when the actual tasks ({@link #mInitialTaskId}, {@link #mSecondTaskId}) are + * to be launched. Call after launcher side animations are complete. + */ + public void launchSplitTasks(Consumer callback) { + Pair instanceIds = + LogUtils.getShellShareableInstanceId(); + launchTasks(mInitialTaskId, mInitialTaskIntent, mSecondTaskId, mSecondTaskIntent, + mInitialStagePosition, callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO, + instanceIds.first); + + mStatsLogManager.logger() + .withItemInfo(mItemInfo) + .withInstanceId(instanceIds.second) + .log(mSplitEvent); + } + + /** + * To be called as soon as user selects the second task (even if animations aren't complete) + * @param task The second task that will be launched. + */ + public void setSecondTask(Task task) { + mSecondTaskId = task.key.id; + } + + /** + * To be called as soon as user selects the second app (even if animations aren't complete) + * @param intent The second intent that will be launched. + * @param user The user of that intent. + */ + public void setSecondTask(Intent intent, UserHandle user) { + mSecondTaskIntent = intent; + mSecondUser = user; } /** * To be called when we want to launch split pairs from an existing GroupedTaskView. */ - public void launchTasks(GroupedTaskView groupedTaskView, - Consumer callback, boolean freezeTaskList) { + public void launchTasks(GroupedTaskView groupedTaskView, Consumer callback, + boolean freezeTaskList) { mLaunchingTaskView = groupedTaskView; TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers = groupedTaskView.getTaskIdAttributeContainers(); - launchTasks(taskIdAttributeContainers[0].getTask(), taskIdAttributeContainers[1].getTask(), + launchTasks(taskIdAttributeContainers[0].getTask().key.id, + taskIdAttributeContainers[1].getTask().key.id, taskIdAttributeContainers[0].getStagePosition(), callback, freezeTaskList, groupedTaskView.getSplitRatio()); } /** - * @param stagePosition representing location of task1 + * To be called when we want to launch split pairs from Overview when split is initiated from + * Overview. */ - public void launchTasks(Task task1, Task task2, @StagePosition int stagePosition, + public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition, Consumer callback, boolean freezeTaskList, float splitRatio) { - // Assume initial task is for top/left part of screen - final int[] taskIds = stagePosition == STAGE_POSITION_TOP_OR_LEFT - ? new int[]{task1.key.id, task2.key.id} - : new int[]{task2.key.id, task1.key.id}; + launchTasks(taskId1, null /* intent1 */, taskId2, null /* intent2 */, stagePosition, + callback, freezeTaskList, splitRatio, null); + } + + /** + * To be called when we want to launch split pairs from Overview. Split can be initiated from + * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a + * fill in intent with a taskId2 are set. + * @param intent1 is null when split is initiated from Overview + * @param stagePosition representing location of task1 + * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that + * create a split instance, null for cases that bring existing instaces to the + * foreground (quickswitch, launching previous pairs from overview) + */ + public void launchTasks(int taskId1, @Nullable Intent intent1, int taskId2, + @Nullable Intent intent2, @StagePosition int stagePosition, + Consumer callback, boolean freezeTaskList, float splitRatio, + @Nullable InstanceId shellInstanceId) { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, "launchSplitTasks"); + final ActivityOptions options1 = ActivityOptions.makeBasic(); + if (freezeTaskList) { + options1.setFreezeRecentTasksReordering(); + } if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - RemoteSplitLaunchTransitionRunner animationRunner = - new RemoteSplitLaunchTransitionRunner(task1, task2); - mSystemUiProxy.startTasks(taskIds[0], null /* mainOptions */, taskIds[1], - null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT, splitRatio, - new RemoteTransitionCompat(animationRunner, MAIN_EXECUTOR, - ActivityThread.currentActivityThread().getApplicationThread())); + final RemoteSplitLaunchTransitionRunner animationRunner = + new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback); + final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, + ActivityThread.currentActivityThread().getApplicationThread()); + if (intent1 == null && intent2 == null) { + mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2, + null /* options2 */, stagePosition, splitRatio, remoteTransition, + shellInstanceId); + } else if (intent2 == null) { + launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition, + splitRatio, remoteTransition, shellInstanceId); + } else if (intent1 == null) { + launchIntentOrShortcut(intent2, mSecondUser, options1, taskId1, + getOppositeStagePosition(stagePosition), splitRatio, remoteTransition, + shellInstanceId); + } else { + mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser), + options1.toBundle(), getPendingIntent(intent2, mSecondUser), + null /* options2 */, stagePosition, splitRatio, remoteTransition, + shellInstanceId); + } } else { - RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(task1, task2, callback); + final RemoteSplitLaunchAnimationRunner animationRunner = + new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback); final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner), - 300, 150, + animationRunner, 300, 150, ActivityThread.currentActivityThread().getApplicationThread()); - ActivityOptions mainOpts = ActivityOptions.makeBasic(); - if (freezeTaskList) { - mainOpts.setFreezeRecentTasksReordering(); + if (intent1 == null && intent2 == null) { + mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(), + taskId2, null /* options2 */, stagePosition, splitRatio, adapter, + shellInstanceId); + } else if (intent2 == null) { + launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2, + stagePosition, splitRatio, adapter, shellInstanceId); + } else if (intent1 == null) { + launchIntentOrShortcutLegacy(intent2, mSecondUser, options1, taskId1, + getOppositeStagePosition(stagePosition), splitRatio, adapter, + shellInstanceId); + } else { + mSystemUiProxy.startIntentsWithLegacyTransition( + getPendingIntent(intent1, mInitialUser), + getShortcutInfo(intent1, mInitialUser), options1.toBundle(), + getPendingIntent(intent2, mSecondUser), + getShortcutInfo(intent2, mSecondUser), null /* options2 */, stagePosition, + splitRatio, adapter, shellInstanceId); } - mSystemUiProxy.startTasksWithLegacyTransition(taskIds[0], mainOpts.toBundle(), - taskIds[1], null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT, - splitRatio, adapter); } } + private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1, + int taskId, @StagePosition int stagePosition, float splitRatio, + RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) { + final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user); + if (shortcutInfo != null) { + mSystemUiProxy.startShortcutAndTask(shortcutInfo, + options1.toBundle(), taskId, null /* options2 */, stagePosition, + splitRatio, remoteTransition, shellInstanceId); + } else { + mSystemUiProxy.startIntentAndTask(getPendingIntent(intent, user), + options1.toBundle(), taskId, null /* options2 */, stagePosition, splitRatio, + remoteTransition, shellInstanceId); + } + } + + private void launchIntentOrShortcutLegacy(Intent intent, UserHandle user, + ActivityOptions options1, int taskId, @StagePosition int stagePosition, + float splitRatio, RemoteAnimationAdapter adapter, + @Nullable InstanceId shellInstanceId) { + final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user); + if (shortcutInfo != null) { + mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo, + options1.toBundle(), taskId, null /* options2 */, stagePosition, + splitRatio, adapter, shellInstanceId); + } else { + mSystemUiProxy.startIntentAndTaskWithLegacyTransition( + getPendingIntent(intent, user), options1.toBundle(), taskId, + null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId); + } + } + + private PendingIntent getPendingIntent(Intent intent, UserHandle user) { + return intent == null ? null : (user != null + ? PendingIntent.getActivityAsUser(mContext, 0, intent, + FLAG_MUTABLE, null /* options */, user) + : PendingIntent.getActivity(mContext, 0, intent, FLAG_MUTABLE)); + } + public @StagePosition int getActiveSplitStagePosition() { - return mStagePosition; + return mInitialStagePosition; + } + + public StatsLogManager.EventEnum getSplitEvent() { + return mSplitEvent; } public void setRecentsAnimationRunning(boolean running) { - this.mRecentsAnimationRunning = running; + mRecentsAnimationRunning = running; + } + + @Nullable + private ShortcutInfo getShortcutInfo(Intent intent, UserHandle user) { + if (intent == null || intent.getPackage() == null) { + return null; + } + + final String shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID); + if (shortcutId == null) { + return null; + } + + try { + final Context context = mContext.createPackageContextAsUser( + intent.getPackage(), 0 /* flags */, user); + return new ShortcutInfo.Builder(context, shortcutId).build(); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage()); + } + + return null; + } + + public boolean isAnimateCurrentTaskDismissal() { + return mAnimateCurrentTaskDismissal; + } + + public void setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal) { + mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; + } + + public boolean isDismissingFromSplitPair() { + return mDismissingFromSplitPair; + } + + public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { + mDismissingFromSplitPair = dismissingFromSplitPair; + } + + public SplitAnimationController getSplitAnimationController() { + return mSplitAnimationController; } /** * Requires Shell Transitions */ - private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner { + private class RemoteSplitLaunchTransitionRunner extends IRemoteTransition.Stub { - private final Task mInitialTask; - private final Task mSecondTask; + private final int mInitialTaskId; + private final int mSecondTaskId; + private final Consumer mSuccessCallback; - RemoteSplitLaunchTransitionRunner(Task initialTask, Task secondTask) { - mInitialTask = initialTask; - mSecondTask = secondTask; + RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, + Consumer callback) { + mInitialTaskId = initialTaskId; + mSecondTaskId = secondTaskId; + mSuccessCallback = callback; } @Override public void startAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, Runnable finishCallback) { - TaskViewUtils.composeRecentsSplitLaunchAnimator(mInitialTask, - mSecondTask, info, t, finishCallback); - // After successful launch, call resetState - resetState(); + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishedCallback) { + final Runnable finishAdapter = () -> { + try { + finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); + } catch (RemoteException e) { + Log.e(TAG, "Failed to call transition finished callback", e); + } + }; + + MAIN_EXECUTOR.execute(() -> { + TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager, + mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> { + finishAdapter.run(); + if (mSuccessCallback != null) { + mSuccessCallback.accept(true); + } + }); + // After successful launch, call resetState + resetState(); + }); } + + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishedCallback) { } } /** * LEGACY * Remote animation runner for animation to launch an app. */ - private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat { + private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat { - private final Task mInitialTask; - private final Task mSecondTask; + private final int mInitialTaskId; + private final int mSecondTaskId; private final Consumer mSuccessCallback; - RemoteSplitLaunchAnimationRunner(Task initialTask, Task secondTask, + RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, Consumer successCallback) { - mInitialTask = initialTask; - mSecondTask = secondTask; + mInitialTaskId = initialTaskId; + mSecondTaskId = secondTaskId; mSuccessCallback = successCallback; } @Override - public void onAnimationStart(int transit, RemoteAnimationTargetCompat[] apps, - RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps, + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { postAsyncCallback(mHandler, () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy( - mLaunchingTaskView, mInitialTask, mSecondTask, apps, wallpapers, + mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers, nonApps, mStateManager, mDepthController, () -> { finishedCallback.run(); if (mSuccessCallback != null) { @@ -208,7 +513,7 @@ public class SplitSelectStateController { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { postAsyncCallback(mHandler, () -> { if (mSuccessCallback != null) { // Launching legacy tasks while recents animation is running will always cause @@ -224,11 +529,19 @@ public class SplitSelectStateController { * To be called if split select was cancelled */ public void resetState() { - mInitialTask = null; - mSecondTask = null; - mStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; + mInitialTaskId = INVALID_TASK_ID; + mInitialTaskIntent = null; + mSecondTaskId = INVALID_TASK_ID; + mSecondTaskIntent = null; + mInitialUser = null; + mSecondUser = null; + mInitialStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; mRecentsAnimationRunning = false; mLaunchingTaskView = null; + mItemInfo = null; + mSplitEvent = null; + mAnimateCurrentTaskDismissal = false; + mDismissingFromSplitPair = false; } /** @@ -236,6 +549,38 @@ public class SplitSelectStateController { * chosen */ public boolean isSplitSelectActive() { - return mInitialTask != null && mSecondTask == null; + return isInitialTaskIntentSet() && !isSecondTaskIntentSet(); + } + + /** + * @return {@code true} if the first and second task have been chosen and split is waiting to + * be launched + */ + public boolean isBothSplitAppsConfirmed() { + return isInitialTaskIntentSet() && isSecondTaskIntentSet(); + } + + private boolean isInitialTaskIntentSet() { + return (mInitialTaskId != INVALID_TASK_ID || mInitialTaskIntent != null); + } + + public int getInitialTaskId() { + return mInitialTaskId; + } + + public int getSecondTaskId() { + return mSecondTaskId; + } + + private boolean isSecondTaskIntentSet() { + return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null); + } + + public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) { + mFirstFloatingTaskView = floatingTaskView; + } + + public FloatingTaskView getFirstFloatingTaskView() { + return mFirstFloatingTaskView; } } diff --git a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java new file mode 100644 index 0000000000..f5b00cf42b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.EMPHASIZED; + +import android.view.animation.Interpolator; + +/** + * Timings for the OverviewSplitSelect > confirmed animation. + */ +abstract class SplitToConfirmTimings implements SplitAnimationTimings { + // Overwritten by device-specific timings + abstract public int getPlaceholderFadeInStart(); + abstract public int getPlaceholderFadeInEnd(); + abstract public int getPlaceholderIconFadeInStart(); + abstract public int getPlaceholderIconFadeInEnd(); + abstract public int getStagedRectSlideStart(); + abstract public int getStagedRectSlideEnd(); + + // Common timings + public int getInstructionsFadeStart() { return 0; } + public int getInstructionsFadeEnd() { return 67; } + public Interpolator getStagedRectXInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectYInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectScaleXInterpolator() { return EMPHASIZED; } + public Interpolator getStagedRectScaleYInterpolator() { return EMPHASIZED; } + + abstract public int getDuration(); + + public float getInstructionsFadeStartOffset() { + return (float) getInstructionsFadeStart() / getDuration(); + } + public float getInstructionsFadeEndOffset() { + return (float) getInstructionsFadeEnd() / getDuration(); + } +} diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java new file mode 100644 index 0000000000..dd10c2da5d --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS; +import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Intent; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.UserHandle; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.quickstep.views.FloatingTaskView; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.InteractionJankMonitorWrapper; + +/** Handles when the stage split lands on the home screen. */ +public class SplitToWorkspaceController { + + private final Launcher mLauncher; + private final DeviceProfile mDP; + private final SplitSelectStateController mController; + + private final int mHalfDividerSize; + + public SplitToWorkspaceController(Launcher launcher, SplitSelectStateController controller) { + mLauncher = launcher; + mDP = mLauncher.getDeviceProfile(); + mController = controller; + + mHalfDividerSize = mLauncher.getResources().getDimensionPixelSize( + R.dimen.multi_window_task_divider_size) / 2; + } + + /** + * Handles second app selection from stage split. If the item can't be opened in split or + * it's not in stage split state, we pass it onto Launcher's default item click handler. + */ + public boolean handleSecondAppSelectionForSplit(View view) { + if ((!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get() + && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) + || !mController.isSplitSelectActive()) { + return false; + } + Object tag = view.getTag(); + Intent intent; + UserHandle user; + BitmapInfo bitmapInfo; + if (tag instanceof WorkspaceItemInfo) { + final WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) tag; + intent = workspaceItemInfo.intent; + user = workspaceItemInfo.user; + bitmapInfo = workspaceItemInfo.bitmap; + } else if (tag instanceof com.android.launcher3.model.data.AppInfo) { + final com.android.launcher3.model.data.AppInfo appInfo = + (com.android.launcher3.model.data.AppInfo) tag; + intent = appInfo.intent; + user = appInfo.user; + bitmapInfo = appInfo.bitmap; + } else { + return false; + } + + mController.setSecondTask(intent, user); + + boolean isTablet = mLauncher.getDeviceProfile().isTablet; + SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet); + PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration()); + + Rect firstTaskStartingBounds = new Rect(); + Rect firstTaskEndingBounds = new Rect(); + RectF secondTaskStartingBounds = new RectF(); + Rect secondTaskEndingBounds = new Rect(); + + RecentsView recentsView = mLauncher.getOverviewPanel(); + recentsView.getPagedOrientationHandler().getFinalSplitPlaceholderBounds(mHalfDividerSize, + mDP, mController.getActiveSplitStagePosition(), firstTaskEndingBounds, + secondTaskEndingBounds); + + FloatingTaskView firstFloatingTaskView = mController.getFirstFloatingTaskView(); + firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds); + firstFloatingTaskView.addConfirmAnimation(pendingAnimation, + new RectF(firstTaskStartingBounds), firstTaskEndingBounds, + false /* fadeWithThumbnail */, true /* isStagedTask */); + + FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher, + view, null /* thumbnail */, bitmapInfo.newIcon(mLauncher), + secondTaskStartingBounds); + secondFloatingTaskView.setAlpha(1); + secondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds, + secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */); + + pendingAnimation.addListener(new AnimatorListenerAdapter() { + private boolean mIsCancelled = false; + + @Override + public void onAnimationCancel(Animator animation) { + mIsCancelled = true; + cleanUp(); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mIsCancelled) { + mController.launchSplitTasks(aBoolean -> cleanUp()); + InteractionJankMonitorWrapper.end( + InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); + } + } + + private void cleanUp() { + mLauncher.getDragLayer().removeView(firstFloatingTaskView); + mLauncher.getDragLayer().removeView(secondFloatingTaskView); + mController.resetState(); + } + }); + pendingAnimation.buildAnim().start(); + return true; + } +} diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java new file mode 100644 index 0000000000..24d832640f --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_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 android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.ActivityManager; +import android.content.Intent; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.SystemClock; +import android.os.UserHandle; +import android.view.View; + +import androidx.annotation.BinderThread; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.R; +import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.OverviewComponentObserver; +import com.android.quickstep.RecentsAnimationCallbacks; +import com.android.quickstep.RecentsAnimationController; +import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.RecentsAnimationTargets; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.views.FloatingTaskView; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +/** Transitions app from fullscreen to stage split when triggered from keyboard shortcuts. */ +public class SplitWithKeyboardShortcutController { + + private final QuickstepLauncher mLauncher; + private final SplitSelectStateController mController; + private final OverviewComponentObserver mOverviewComponentObserver; + + private final int mSplitPlaceholderSize; + private final int mSplitPlaceholderInset; + + public SplitWithKeyboardShortcutController(QuickstepLauncher launcher, + SplitSelectStateController controller) { + mLauncher = launcher; + mController = controller; + RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState( + launcher.getApplicationContext()); + mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(), + deviceState); + + mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( + R.dimen.split_placeholder_size); + mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize( + R.dimen.split_placeholder_inset); + } + + @BinderThread + public void enterStageSplit(boolean leftOrTop) { + if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()) { + return; + } + RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), + false /* allowMinimizeSplitScreen */); + 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)); + }); + } + + public void onDestroy() { + mOverviewComponentObserver.onDestroy(); + } + + private class SplitWithKeyboardShortcutRecentsAnimationListener implements + RecentsAnimationCallbacks.RecentsAnimationListener { + + private final boolean mLeftOrTop; + private final Rect mTempRect = new Rect(); + + private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) { + mLeftOrTop = leftOrTop; + } + + @Override + public void onRecentsAnimationStart(RecentsAnimationController controller, + RecentsAnimationTargets targets) { + ActivityManager.RunningTaskInfo runningTaskInfo = + ActivityManagerWrapper.getInstance().getRunningTask(); + mController.setInitialTaskSelect(runningTaskInfo, + mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT, + null /* itemInfo */, + mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP + : LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM); + + RecentsView recentsView = mLauncher.getOverviewPanel(); + recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds( + mSplitPlaceholderSize, mSplitPlaceholderInset, mLauncher.getDeviceProfile(), + mController.getActiveSplitStagePosition(), mTempRect); + + PendingAnimation anim = new PendingAnimation( + SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration()); + RectF startingTaskRect = new RectF(); + final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( + mLauncher, mLauncher.getDragLayer(), + controller.screenshotTask(runningTaskInfo.taskId).thumbnail, + null /* icon */, startingTaskRect); + RecentsModel.INSTANCE.get(mLauncher.getApplicationContext()) + .getIconCache() + .updateIconInBackground( + Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo, + false /* isLocked */), + (task) -> { + if (task.thumbnail != null) { + floatingTaskView.setIcon(task.thumbnail.thumbnail); + } + }); + floatingTaskView.setAlpha(1); + floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, + false /* fadeWithThumbnail */, true /* isStagedTask */); + mController.setFirstFloatingTaskView(floatingTaskView); + + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + controller.finish(true /* toRecents */, null /* onFinishComplete */, + false /* sendUserLeaveHint */); + } + }); + anim.buildAnim().start(); + } + }; +} diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java index 44396faa49..cd5edab9d6 100644 --- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java +++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java @@ -18,6 +18,7 @@ package com.android.quickstep.util; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER; @@ -34,19 +35,21 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Hotseat; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Workspace; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DynamicResource; import com.android.quickstep.views.RecentsView; import com.android.systemui.plugins.ResourceProvider; @@ -59,10 +62,10 @@ import com.android.systemui.plugins.ResourceProvider; public class StaggeredWorkspaceAnim { private static final int APP_CLOSE_ROW_START_DELAY_MS = 10; - // How long it takes to fade in each staggered row. - private static final int ALPHA_DURATION_MS = 250; // Should be used for animations running alongside this StaggeredWorkspaceAnim. public static final int DURATION_MS = 250; + public static final int DURATION_TASKBAR_MS = + QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION; private static final float MAX_VELOCITY_PX_PER_S = 22f; @@ -90,16 +93,20 @@ public class StaggeredWorkspaceAnim { mSpringTransY = transFactor * launcher.getResources() .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y); + DeviceProfile grid = launcher.getDeviceProfile(); + long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS; if (staggerWorkspace) { - DeviceProfile grid = launcher.getDeviceProfile(); - Workspace workspace = launcher.getWorkspace(); + Workspace workspace = launcher.getWorkspace(); Hotseat hotseat = launcher.getHotseat(); - // Hotseat and QSB takes up two additional rows. - int totalRows = grid.inv.numRows + (grid.isVerticalBarLayout() ? 0 : 2); + boolean staggerHotseat = !grid.isVerticalBarLayout() && !grid.isTaskbarPresent; + boolean staggerQsb = + !grid.isVerticalBarLayout() && !(grid.isTaskbarPresent && grid.isQsbInline); + int totalRows = grid.inv.numRows + (staggerHotseat ? 1 : 0) + (staggerQsb ? 1 : 0); // Add animation for all the visible workspace pages - workspace.forEachVisiblePage(page -> addAnimationForPage((CellLayout) page, totalRows)); + workspace.forEachVisiblePage( + page -> addAnimationForPage((CellLayout) page, totalRows, duration)); boolean workspaceClipChildren = workspace.getClipChildren(); boolean workspaceClipToPadding = workspace.getClipToPadding(); @@ -116,25 +123,35 @@ public class StaggeredWorkspaceAnim { if (grid.isVerticalBarLayout()) { for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) { View child = hotseatIcons.getChildAt(i); - CellLayout.LayoutParams lp = - ((CellLayout.LayoutParams) child.getLayoutParams()); - addStaggeredAnimationForView(child, lp.cellY + 1, totalRows); + CellLayoutLayoutParams lp = ((CellLayoutLayoutParams) child.getLayoutParams()); + addStaggeredAnimationForView(child, lp.getCellY() + 1, totalRows, duration); } } else { final int hotseatRow, qsbRow; if (grid.isTaskbarPresent) { - qsbRow = grid.inv.numRows + 1; - hotseatRow = grid.inv.numRows + 2; + if (grid.isQsbInline) { + qsbRow = grid.inv.numRows + 1; + hotseatRow = grid.inv.numRows + 1; + } else { + qsbRow = grid.inv.numRows + 1; + hotseatRow = grid.inv.numRows + 2; + } } else { hotseatRow = grid.inv.numRows + 1; qsbRow = grid.inv.numRows + 2; } - for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) { - View child = hotseatIcons.getChildAt(i); - addStaggeredAnimationForView(child, hotseatRow, totalRows); - } - addStaggeredAnimationForView(hotseat.getQsb(), qsbRow, totalRows); + // Do not stagger hotseat as a whole when taskbar is present, and stagger QSB only + // if it's not inline. + if (staggerHotseat) { + for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) { + View child = hotseatIcons.getChildAt(i); + addStaggeredAnimationForView(child, hotseatRow, totalRows, duration); + } + } + if (staggerQsb) { + addStaggeredAnimationForView(hotseat.getQsb(), qsbRow, totalRows, duration); + } } mAnimators.addListener(new AnimatorListenerAdapter() { @@ -148,20 +165,23 @@ public class StaggeredWorkspaceAnim { }); } + launcher.pauseExpensiveViewUpdates(); + mAnimators.addListener(forEndCallback(launcher::resumeExpensiveViewUpdates)); + if (animateOverviewScrim) { - PendingAnimation pendingAnimation = new PendingAnimation(DURATION_MS); + PendingAnimation pendingAnimation = new PendingAnimation(duration); launcher.getWorkspace().getStateTransitionAnimation() .setScrim(pendingAnimation, NORMAL, new StateAnimationConfig()); mAnimators.play(pendingAnimation.buildAnim()); } - addDepthAnimationForState(launcher, NORMAL, DURATION_MS); + addDepthAnimationForState(launcher, NORMAL, duration); mAnimators.play(launcher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f) - .setDuration(DURATION_MS)); + .setDuration(duration)); } - private void addAnimationForPage(CellLayout page, int totalRows) { + private void addAnimationForPage(CellLayout page, int totalRows, long duration) { ShortcutAndWidgetContainer itemsContainer = page.getShortcutsAndWidgets(); boolean pageClipChildren = page.getClipChildren(); @@ -173,8 +193,8 @@ public class StaggeredWorkspaceAnim { // Set up springs on workspace items. for (int i = itemsContainer.getChildCount() - 1; i >= 0; i--) { View child = itemsContainer.getChildAt(i); - CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams()); - addStaggeredAnimationForView(child, lp.cellY + lp.cellVSpan, totalRows); + CellLayoutLayoutParams lp = ((CellLayoutLayoutParams) child.getLayoutParams()); + addStaggeredAnimationForView(child, lp.getCellY() + lp.cellVSpan, totalRows, duration); } mAnimators.addListener(new AnimatorListenerAdapter() { @@ -227,8 +247,9 @@ public class StaggeredWorkspaceAnim { * @param v A view on the workspace. * @param row The bottom-most row that contains the view. * @param totalRows Total number of rows. + * @param duration duration of the animation */ - private void addStaggeredAnimationForView(View v, int row, int totalRows) { + private void addStaggeredAnimationForView(View v, int row, int totalRows, long duration) { if (mIgnoredView != null && mIgnoredView == v) return; // Invert the rows, because we stagger starting from the bottom of the screen. int invertedRow = totalRows - row; @@ -262,7 +283,7 @@ public class StaggeredWorkspaceAnim { v.setAlpha(0); ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f); alpha.setInterpolator(LINEAR); - alpha.setDuration(ALPHA_DURATION_MS); + alpha.setDuration(duration); alpha.setStartDelay(startDelay); alpha.addListener(new AnimatorListenerAdapter() { @Override @@ -274,11 +295,11 @@ public class StaggeredWorkspaceAnim { } private void addDepthAnimationForState(Launcher launcher, LauncherState state, long duration) { - if (!(launcher instanceof BaseQuickstepLauncher)) { + if (!(launcher instanceof QuickstepLauncher)) { return; } PendingAnimation builder = new PendingAnimation(duration); - DepthController depthController = ((BaseQuickstepLauncher) launcher).getDepthController(); + DepthController depthController = ((QuickstepLauncher) launcher).getDepthController(); depthController.setStateWithAnimation(state, new StateAnimationConfig(), builder); mAnimators.play(builder.buildAnim()); } diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java new file mode 100644 index 0000000000..7ab285dfa7 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; + +/** + * Helper class for building a {@link Transaction}. + */ +public class SurfaceTransaction { + + private final Transaction mTransaction = new Transaction(); + private final float[] mTmpValues = new float[9]; + + /** + * Creates a new builder for the provided surface + */ + public SurfaceProperties forSurface(SurfaceControl surface) { + return surface.isValid() ? new SurfaceProperties(surface) : new MockProperties(); + } + + /** + * Returns the final transaction + */ + public Transaction getTransaction() { + return mTransaction; + } + + /** + * Utility class to update surface params in a transaction + */ + public class SurfaceProperties { + + private final SurfaceControl mSurface; + + SurfaceProperties(SurfaceControl surface) { + mSurface = surface; + } + + /** + * @param alpha The alpha value to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setAlpha(float alpha) { + mTransaction.setAlpha(mSurface, alpha); + return this; + } + + /** + * @param matrix The matrix to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setMatrix(Matrix matrix) { + mTransaction.setMatrix(mSurface, matrix, mTmpValues); + return this; + } + + /** + * @param windowCrop The window crop to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setWindowCrop(Rect windowCrop) { + mTransaction.setWindowCrop(mSurface, windowCrop); + return this; + } + + /** + * @param relativeLayer The relative layer. + * @return this Builder + */ + public SurfaceProperties setLayer(int relativeLayer) { + mTransaction.setLayer(mSurface, relativeLayer); + return this; + } + + /** + * @param radius the Radius for rounded corners to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setCornerRadius(float radius) { + mTransaction.setCornerRadius(mSurface, radius); + return this; + } + + /** + * @param radius the Radius for the shadows to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setShadowRadius(float radius) { + mTransaction.setShadowRadius(mSurface, radius); + return this; + } + } + + /** + * Extension of {@link SurfaceProperties} which just stores all the values locally + */ + public class MockProperties extends SurfaceProperties { + + public float alpha = -1; + public Matrix matrix = null; + public Rect windowCrop = null; + public float cornerRadius = 0; + public float shadowRadius = 0; + + protected MockProperties() { + super(null); + } + + @Override + public SurfaceProperties setAlpha(float alpha) { + this.alpha = alpha; + return this; + } + + @Override + public SurfaceProperties setMatrix(Matrix matrix) { + this.matrix = matrix; + return this; + } + + @Override + public SurfaceProperties setWindowCrop(Rect windowCrop) { + this.windowCrop = windowCrop; + return this; + } + + @Override + public SurfaceProperties setLayer(int relativeLayer) { + return this; + } + + @Override + public SurfaceProperties setCornerRadius(float radius) { + this.cornerRadius = radius; + return this; + } + + @Override + public SurfaceProperties setShadowRadius(float radius) { + this.shadowRadius = radius; + return this; + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java index 66922bf001..95473dc39a 100644 --- a/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java +++ b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; import android.annotation.TargetApi; -import android.graphics.HardwareRenderer; import android.os.Build; import android.os.Handler; import android.os.Message; @@ -26,7 +25,6 @@ import android.view.View; import android.view.ViewRootImpl; import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; import java.util.function.Consumer; @@ -54,6 +52,7 @@ public class SurfaceTransactionApplier extends ReleaseCheck { mTargetViewRootImpl = targetView.getViewRootImpl(); mBarrierSurfaceControl = mTargetViewRootImpl.getSurfaceControl(); mApplyHandler = new Handler(this::onApplyMessage); + setCanRelease(true); } protected boolean onApplyMessage(Message msg) { @@ -70,34 +69,25 @@ public class SurfaceTransactionApplier extends ReleaseCheck { * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into * this method to avoid synchronization issues. */ - public void scheduleApply(final SurfaceParams... params) { + public void scheduleApply(SurfaceTransaction params) { View view = mTargetViewRootImpl.getView(); if (view == null) { return; } + Transaction t = params.getTransaction(); mLastSequenceNumber++; final int toApplySeqNo = mLastSequenceNumber; setCanRelease(false); - mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() { - @Override - public void onFrameDraw(long frame) { - if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) { - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - return; - } - Transaction t = new Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SurfaceParams surfaceParams = params[i]; - if (surfaceParams.surface.isValid()) { - surfaceParams.applyTo(t); - } - } - mTargetViewRootImpl.mergeWithNextTransaction(t, frame); + mTargetViewRootImpl.registerRtFrameCallback(frame -> { + if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) { Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) .sendToTarget(); + return; } + mTargetViewRootImpl.mergeWithNextTransaction(t, frame); + Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) + .sendToTarget(); }); // Make sure a frame gets scheduled. @@ -105,7 +95,7 @@ public class SurfaceTransactionApplier extends ReleaseCheck { } /** - * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is + * Creates an instance of SurfaceTransactionApplier, deferring until the target view is * attached if necessary. */ public static void create( diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java index a534450e4e..1112f4dd9e 100644 --- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java +++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java @@ -22,27 +22,26 @@ import android.animation.Animator; import android.animation.RectEvaluator; import android.content.ComponentName; import android.content.Context; -import android.graphics.Color; +import android.content.pm.ActivityInfo; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; +import android.os.SystemProperties; import android.util.Log; import android.view.Surface; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.View; import android.window.PictureInPictureSurfaceTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; -import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.util.Themes; +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; /** * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window @@ -54,8 +53,9 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private static final float END_PROGRESS = 1.0f; private final int mTaskId; - private final ComponentName mComponentName; + private final ActivityInfo mActivityInfo; private final SurfaceControl mLeash; + private final Rect mSourceRectHint = new Rect(); private final Rect mAppBounds = new Rect(); private final Matrix mHomeToWindowPositionMap = new Matrix(); private final Rect mStartBounds = new Rect(); @@ -64,7 +64,10 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private final Rect mDestinationBounds = new Rect(); private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - /** for calculating transform in {@link #onAnimationUpdate(AppCloseConfig, RectF, float)} */ + /** + * For calculating transform in + * {@link #onAnimationUpdate(SurfaceControl.Transaction, RectF, float)} + */ private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); private final Rect mSourceHintRectInsets; private final Rect mSourceInsets = new Rect(); @@ -80,16 +83,18 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private boolean mHasAnimationEnded; /** - * An overlay used to mask changes in content when entering PiP for apps that aren't seamless. + * Wrapper of {@link SurfaceControl} that is used when entering PiP without valid + * source rect hint. */ @Nullable - private SurfaceControl mContentOverlay; + private PipContentOverlay mPipContentOverlay; /** * @param context {@link Context} provides Launcher resources * @param taskId Task id associated with this animator, see also {@link #getTaskId()} - * @param componentName Component associated with this animator, + * @param activityInfo {@link ActivityInfo} associated with this animator, * see also {@link #getComponentName()} + * @param appIconSizePx The size in pixel for the app icon in content overlay * @param leash {@link SurfaceControl} this animator operates on * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams} * @param appBounds Bounds of the application, sourceRectHint is based on this bounds @@ -101,11 +106,13 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { * @param fromRotation From rotation if different from final rotation, ROTATION_0 otherwise * @param destinationBoundsTransformed Destination bounds in window space * @param cornerRadius Corner radius in pixel value for PiP window + * @param shadowRadius Shadow radius in pixel value for PiP window * @param view Attached view for logging purpose */ private SwipePipToHomeAnimator(@NonNull Context context, int taskId, - @NonNull ComponentName componentName, + @NonNull ActivityInfo activityInfo, + int appIconSizePx, @NonNull SurfaceControl leash, @Nullable Rect sourceRectHint, @NonNull Rect appBounds, @@ -115,10 +122,12 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { @RecentsOrientedState.SurfaceRotation int fromRotation, @NonNull Rect destinationBoundsTransformed, int cornerRadius, + int shadowRadius, @NonNull View view) { - super(startBounds, new RectF(destinationBoundsTransformed), context, null); + super(new DefaultSpringConfig(context, null, startBounds, + new RectF(destinationBoundsTransformed))); mTaskId = taskId; - mComponentName = componentName; + mActivityInfo = activityInfo; mLeash = leash; mAppBounds.set(appBounds); mHomeToWindowPositionMap.set(homeToWindowPositionMap); @@ -126,7 +135,7 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { mDestinationBounds.set(destinationBounds); mFromRotation = fromRotation; mDestinationBoundsTransformed.set(destinationBoundsTransformed); - mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius); + mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius); if (sourceRectHint != null && (sourceRectHint.width() < destinationBounds.width() || sourceRectHint.height() < destinationBounds.height())) { @@ -138,35 +147,24 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { } if (sourceRectHint == null) { + mSourceRectHint.setEmpty(); mSourceHintRectInsets = null; - // Create a new overlay layer - SurfaceSession session = new SurfaceSession(); - mContentOverlay = new SurfaceControl.Builder(session) - .setCallsite("SwipePipToHomeAnimator") - .setName("PipContentOverlay") - .setColorLayer() - .build(); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - t.show(mContentOverlay); - t.setLayer(mContentOverlay, Integer.MAX_VALUE); - int color = Themes.getColorBackground(view.getContext()); - float[] bgColor = new float[] {Color.red(color) / 255f, Color.green(color) / 255f, - Color.blue(color) / 255f}; - t.setColor(mContentOverlay, bgColor); - t.setAlpha(mContentOverlay, 0f); - t.reparent(mContentOverlay, mLeash); - t.apply(); - - addOnUpdateListener((currentRect, progress) -> { - float alpha = progress < 0.5f - ? 0 - : Utilities.mapToRange(Math.min(progress, 1f), 0.5f, 1f, - 0f, 1f, Interpolators.FAST_OUT_SLOW_IN); - t.setAlpha(mContentOverlay, alpha); - t.apply(); - }); + // 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. + if (SystemProperties.getBoolean( + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { + mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(), + mAppBounds, new IconProvider(context).getIcon(mActivityInfo), + appIconSizePx); + } else { + mPipContentOverlay = new PipContentOverlay.PipColorOverlay(view.getContext()); + } + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mPipContentOverlay.attach(tx, mLeash); } else { + mSourceRectHint.set(sourceRectHint); mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left, sourceRectHint.top - appBounds.top, appBounds.right - sourceRectHint.right, @@ -213,6 +211,9 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx, RectF currentRect, float progress) { currentRect.round(mCurrentBounds); + if (mPipContentOverlay != null) { + mPipContentOverlay.onAnimationUpdate(tx, mCurrentBounds, progress); + } final PictureInPictureSurfaceTransaction op; if (mSourceHintRectInsets == null) { // no source rect hint been set, directly scale the window down @@ -247,7 +248,8 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets, rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY); } else { - return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets); + return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, mAppBounds, + bounds, insets, progress); } } @@ -256,7 +258,7 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { } public ComponentName getComponentName() { - return mComponentName; + return mActivityInfo.getComponentName(); } public Rect getDestinationBounds() { @@ -265,7 +267,7 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { @Nullable public SurfaceControl getContentOverlay() { - return mContentOverlay; + return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash(); } /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */ @@ -273,12 +275,15 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { // get the final leash operations but do not apply to the leash. final SurfaceControl.Transaction tx = PipSurfaceTransactionHelper.newSurfaceControlTransaction(); - return onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS); + final PictureInPictureSurfaceTransaction pipTx = + onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS); + pipTx.setShouldDisableCanAffectSystemUiFlags(true); + return pipTx; } private RotatedPosition getRotatedPosition(float progress) { final float degree, positionX, positionY; - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { + if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) { if (mFromRotation == Surface.ROTATION_90) { degree = -90 * (1 - progress); positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) @@ -315,7 +320,8 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { public static class Builder { private Context mContext; private int mTaskId; - private ComponentName mComponentName; + private ActivityInfo mActivityInfo; + private int mAppIconSizePx; private SurfaceControl mLeash; private Rect mSourceRectHint; private Rect mDisplayCutoutInsets; @@ -324,6 +330,7 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private RectF mStartBounds; private Rect mDestinationBounds; private int mCornerRadius; + private int mShadowRadius; private View mAttachedView; private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0; private final Rect mDestinationBoundsTransformed = new Rect(); @@ -338,8 +345,13 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { return this; } - public Builder setComponentName(ComponentName componentName) { - mComponentName = componentName; + public Builder setActivityInfo(ActivityInfo activityInfo) { + mActivityInfo = activityInfo; + return this; + } + + public Builder setAppIconSizePx(int appIconSizePx) { + mAppIconSizePx = appIconSizePx; return this; } @@ -378,6 +390,11 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { return this; } + public Builder setShadowRadius(int shadowRadius) { + mShadowRadius = shadowRadius; + return this; + } + public Builder setAttachedView(View attachedView) { mAttachedView = attachedView; return this; @@ -418,11 +435,11 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { mAppBounds.inset(mDisplayCutoutInsets); } } - return new SwipePipToHomeAnimator(mContext, mTaskId, mComponentName, mLeash, - mSourceRectHint, mAppBounds, + return new SwipePipToHomeAnimator(mContext, mTaskId, mActivityInfo, mAppIconSizePx, + mLeash, mSourceRectHint, mAppBounds, mHomeToWindowPositionMap, mStartBounds, mDestinationBounds, mFromRotation, mDestinationBoundsTransformed, - mCornerRadius, mAttachedView); + mCornerRadius, mShadowRadius, mAttachedView); } } diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java new file mode 100644 index 0000000000..a34888f684 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.util.ArrayMap; +import android.view.Surface; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import com.android.internal.policy.SystemBarUtils; +import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.window.CachedDisplayInfo; +import com.android.launcher3.util.window.WindowManagerProxy; + +import java.util.Set; + +/** + * Extension of {@link WindowManagerProxy} with some assumption for the default system Launcher + */ +public class SystemWindowManagerProxy extends WindowManagerProxy { + + public SystemWindowManagerProxy(Context context) { + super(true); + } + + @Override + public int getRotation(Context displayInfoContext) { + return displayInfoContext.getResources().getConfiguration().windowConfiguration + .getRotation(); + } + + @Override + protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) { + // See b/264656380, calculate the status bar height manually as the inset in the system + // server might not be updated by this point yet causing extra DeviceProfile updates + return SystemBarUtils.getStatusBarHeight(context); + } + + @Override + public ArrayMap estimateInternalDisplayBounds( + Context displayInfoContext) { + ArrayMap result = new ArrayMap<>(); + WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class); + Set possibleMaximumWindowMetrics = + windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) { + CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0); + WindowBounds[] bounds = estimateWindowBounds(displayInfoContext, info); + result.put(info, bounds); + } + return result; + } +} diff --git a/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java b/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java new file mode 100644 index 0000000000..bf8612a797 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.LINEAR; + +import android.view.animation.Interpolator; + +/** + * Timings for the Home > OverviewSplitSelect animation on tablets. + */ +public class TabletHomeToSplitTimings + extends TabletOverviewToSplitTimings implements SplitAnimationTimings { + @Override + public Interpolator getStagedRectXInterpolator() { return LINEAR; } + @Override + public Interpolator getStagedRectScaleXInterpolator() { return LINEAR; } + @Override + public Interpolator getStagedRectScaleYInterpolator() { return LINEAR; } + + public int getScrimFadeInStart() { return 0; } + public int getScrimFadeInEnd() { return 167; } + + public float getScrimFadeInStartOffset() { + return (float) getScrimFadeInStart() / getDuration(); + } + public float getScrimFadeInEndOffset() { + return (float) getScrimFadeInEnd() / getDuration(); + } +} diff --git a/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java new file mode 100644 index 0000000000..cbf46bfcb8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; + +import android.view.animation.Interpolator; + +/** + * Timings for the Overview > OverviewSplitSelect animation on tablets. + */ +public class TabletOverviewToSplitTimings + extends OverviewToSplitTimings implements SplitAnimationTimings { + public int getPlaceholderFadeInStart() { return 0; } + public int getPlaceholderFadeInEnd() { return 133; } + public int getPlaceholderIconFadeInStart() { return 167; } + public int getPlaceholderIconFadeInEnd() { return 250; } + public int getStagedRectSlideStart() { return 0; } + public int getStagedRectSlideEnd() { return 417; } + public int getGridSlideStart() { return 67; } + public int getGridSlideStagger() { return 16; } + public int getGridSlideDuration() { return 500; } + + public int getDuration() { return TABLET_ENTER_DURATION; } + public Interpolator getStagedRectXInterpolator() { return DEACCEL_2; } + public Interpolator getStagedRectYInterpolator() { return DEACCEL_2; } + public Interpolator getStagedRectScaleXInterpolator() { return DEACCEL_2; } + public Interpolator getStagedRectScaleYInterpolator() { return DEACCEL_2; } +} diff --git a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java new file mode 100644 index 0000000000..3756b4af59 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +/** + * Timings for the OverviewSplitSelect > confirmed animation on tablets. + */ +public class TabletSplitToConfirmTimings + extends SplitToConfirmTimings implements SplitAnimationTimings { + public int getPlaceholderFadeInStart() { return 0; } + public int getPlaceholderFadeInEnd() { return 133; } + public int getPlaceholderIconFadeInStart() { return 167; } + public int getPlaceholderIconFadeInEnd() { return 250; } + public int getStagedRectSlideStart() { return 0; } + public int getStagedRectSlideEnd() { return 500; } + + public int getDuration() { return TABLET_CONFIRM_DURATION; } +} diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index f676091607..f8893bd9dd 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -15,16 +15,17 @@ */ package com.android.quickstep.util; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + import static com.android.launcher3.states.RotationHelper.deltaRotation; import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE; 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.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.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; import android.animation.TimeInterpolator; import android.content.Context; @@ -34,21 +35,22 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; +import android.view.RemoteAnimationTarget; import androidx.annotation.NonNull; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.launcher3.util.TraceHelper; -import com.android.quickstep.AnimatedFloat; import com.android.quickstep.BaseActivityInterface; -import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper; +import com.android.quickstep.TaskAnimationManager; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.views.TaskView.FullscreenDrawParams; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder; +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; /** * A utility class which emulates the layout behavior of TaskView and RecentsView @@ -99,9 +101,10 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { // Cached calculations private boolean mLayoutValid = false; private int mOrientationStateId; - private StagedSplitBounds mStagedSplitBounds; - private boolean mDrawsBelowRecents; + private SplitBounds mSplitBounds; + private Boolean mDrawsBelowRecents = null; private boolean mIsGridTask; + private boolean mIsDesktopTask; private int mTaskRectTranslationX; private int mTaskRectTranslationY; @@ -143,21 +146,29 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { if (mDp == null) { return 1; } + + if (mIsDesktopTask) { + mTaskRect.set(mThumbnailPosition); + mPivot.set(mTaskRect.centerX(), mTaskRect.centerY()); + return 1; + } + if (mIsGridTask) { mSizeStrategy.calculateGridTaskSize(mContext, mDp, mTaskRect, mOrientationState.getOrientationHandler()); } else { - mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect); + mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect, + mOrientationState.getOrientationHandler()); } Rect fullTaskSize; - if (mStagedSplitBounds != null) { + if (mSplitBounds != null) { // The task rect changes according to the staged split task sizes, but recents // fullscreen scale and pivot remains the same since the task fits into the existing // sized task space bounds fullTaskSize = new Rect(mTaskRect); mOrientationState.getOrientationHandler() - .setSplitTaskSwipeRect(mDp, mTaskRect, mStagedSplitBounds, mStagePosition); + .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition); mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); } else { fullTaskSize = mTaskRect; @@ -169,8 +180,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { /** * Sets the targets which the simulator will control */ - public void setPreview(RemoteAnimationTargetCompat runningTarget) { - setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets); + public void setPreview(RemoteAnimationTarget runningTarget) { + setPreviewBounds( + runningTarget.startBounds == null + ? runningTarget.screenSpaceBounds : runningTarget.startBounds, + runningTarget.contentInsets); } /** @@ -179,16 +193,17 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { * * @param splitInfo set to {@code null} when not in staged split mode */ - public void setPreview(RemoteAnimationTargetCompat runningTarget, StagedSplitBounds splitInfo) { + public void setPreview(RemoteAnimationTarget runningTarget, SplitBounds splitInfo) { setPreview(runningTarget); - mStagedSplitBounds = splitInfo; - if (mStagedSplitBounds == null) { + mSplitBounds = splitInfo; + if (mSplitBounds == null) { mStagePosition = STAGE_POSITION_UNDEFINED; return; } mStagePosition = mThumbnailPosition.equals(splitInfo.leftTopBounds) ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; + mPositionHelper.setSplitBounds(convertSplitBounds(mSplitBounds), mStagePosition); } /** @@ -221,6 +236,13 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mIsGridTask = isGridTask; } + /** + * Sets whether this task is part of desktop tasks in overview. + */ + public void setIsDesktopTask(boolean desktop) { + mIsDesktopTask = desktop; + } + /** * Apply translations on TaskRect's starting location. */ @@ -250,9 +272,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { */ public RectF getCurrentCropRect() { // Crop rect is the inverse of thumbnail matrix - RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets; - mTempRectF.set(-insets.left, -insets.top, - mTaskRect.width() + insets.right, mTaskRect.height() + insets.bottom); + mTempRectF.set(0, 0, mTaskRect.width(), mTaskRect.height()); mInversePositionMatrix.mapRect(mTempRectF); return mTempRectF; } @@ -304,14 +324,20 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mOrientationStateId = mOrientationState.getStateId(); getFullScreenScale(); - mThumbnailData.rotation = mOrientationState.getDisplayRotation(); + if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) { + // With shell transitions, the display is rotated early so we need to actually use + // the rotation when the gesture starts + mThumbnailData.rotation = mOrientationState.getTouchRotation(); + } else { + mThumbnailData.rotation = mOrientationState.getDisplayRotation(); + } // mIsRecentsRtl is the inverse of TaskView RTL. boolean isRtlEnabled = !mIsRecentsRtl; mPositionHelper.updateThumbnailMatrix( - mThumbnailPosition, mThumbnailData, - mTaskRect.width(), mTaskRect.height(), - mDp, mOrientationState.getRecentsActivityRotation(), isRtlEnabled); + mThumbnailPosition, mThumbnailData, mTaskRect.width(), mTaskRect.height(), + mDp.widthPx, mDp.heightPx, mDp.taskbarHeight, mDp.isTablet, + mOrientationState.getRecentsActivityRotation(), isRtlEnabled); mPositionHelper.getMatrix().invert(mInversePositionMatrix); if (DEBUG) { Log.d(TAG, " taskRect: " + mTaskRect); @@ -323,14 +349,10 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { /* taskViewScale= */1f, mTaskRect.width(), mDp, mPositionHelper); // Apply thumbnail matrix - RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets; - float scale = mCurrentFullscreenParams.mScale; float taskWidth = mTaskRect.width(); float taskHeight = mTaskRect.height(); mMatrix.set(mPositionHelper.getMatrix()); - mMatrix.postTranslate(insets.left, insets.top); - mMatrix.postScale(scale, scale); // Apply TaskView matrix: taskRect, translate mMatrix.postTranslate(mTaskRect.left, mTaskRect.top); @@ -350,8 +372,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { applyWindowToHomeRotation(mMatrix); // Crop rect is the inverse of thumbnail matrix - mTempRectF.set(-insets.left, -insets.top, - taskWidth + insets.right, taskHeight + insets.bottom); + mTempRectF.set(0, 0, taskWidth, taskHeight); mInversePositionMatrix.mapRect(mTempRectF); mTempRectF.roundOut(mTmpCropRect); @@ -361,7 +382,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return; } Log.d(TAG, "progress: " + fullScreenProgress - + " scale: " + scale + " recentsViewScale: " + recentsViewScale.value + " crop: " + mTmpCropRect + " radius: " + getCurrentCornerRadius() @@ -378,15 +398,21 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { @Override public void onBuildTargetParams( - Builder builder, RemoteAnimationTargetCompat app, TransformParams params) { - builder.withMatrix(mMatrix) - .withWindowCrop(mTmpCropRect) - .withCornerRadius(getCurrentCornerRadius()); + SurfaceProperties builder, RemoteAnimationTarget app, TransformParams params) { + builder.setMatrix(mMatrix) + .setWindowCrop(mTmpCropRect) + .setCornerRadius(getCurrentCornerRadius()); - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) { - // When relativeLayer = 0, it reverts the surfaces back to the original order. - builder.withRelativeLayerTo(params.getRecentsSurface(), - mDrawsBelowRecents ? Integer.MIN_VALUE : 0); + // 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. + builder.setLayer(mDrawsBelowRecents + ? Integer.MIN_VALUE + 1 + : ENABLE_SHELL_TRANSITIONS ? Integer.MAX_VALUE : 0); } } @@ -404,4 +430,15 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return Math.max(Math.abs(mTempPoint[0]), Math.abs(mTempPoint[1])); } + /** + * TODO(b/254378592): Remove this after consolidation of classes + */ + public static com.android.wm.shell.util.SplitBounds convertSplitBounds(SplitBounds bounds) { + return new com.android.wm.shell.util.SplitBounds( + bounds.leftTopBounds, + bounds.rightBottomBounds, + bounds.leftTopTaskId, + bounds.rightBottomTaskId + ); + } } diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java new file mode 100644 index 0000000000..66bff730bf --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import android.os.UserHandle; + +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +/** + * Listener for receiving various task properties changes + */ +public interface TaskVisualsChangeListener { + + /** + * Called when the task thumbnail changes + */ + default Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { + return null; + } + + /** + * Called when the icon for a task changes + */ + default void onTaskIconChanged(String pkg, UserHandle user) {} + + /** + * Called when the icon for a task changes + */ + default void onTaskIconChanged(int taskId) {} +} diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java index 03d7a37748..aa9a45bd8b 100644 --- a/quickstep/src/com/android/quickstep/util/TransformParams.java +++ b/quickstep/src/com/android/quickstep/util/TransformParams.java @@ -15,16 +15,18 @@ */ package com.android.quickstep.util; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; + import android.util.FloatProperty; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import com.android.launcher3.Utilities; import com.android.launcher3.anim.Interpolators; import com.android.quickstep.RemoteAnimationTargets; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; -import com.android.systemui.shared.system.TransactionCompat; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; public class TransformParams { @@ -113,8 +115,7 @@ public class TransformParams { * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that * are computed based on these TransformParams. */ - public TransformParams setSyncTransactionApplier( - SurfaceTransactionApplier applier) { + public TransformParams setSyncTransactionApplier(SurfaceTransactionApplier applier) { mSyncTransactionApplier = applier; return this; } @@ -137,26 +138,26 @@ public class TransformParams { return this; } - public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) { + public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) { RemoteAnimationTargets targets = mTargetSet; - SurfaceParams[] surfaceParams = new SurfaceParams[targets.unfilteredApps.length]; + SurfaceTransaction transaction = new SurfaceTransaction(); mRecentsSurface = getRecentsSurface(targets); for (int i = 0; i < targets.unfilteredApps.length; i++) { - RemoteAnimationTargetCompat app = targets.unfilteredApps[i]; - SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash); + RemoteAnimationTarget app = targets.unfilteredApps[i]; + SurfaceProperties builder = transaction.forSurface(app.leash); if (app.mode == targets.targetMode) { - if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + int activityType = app.windowConfiguration.getActivityType(); + if (activityType == ACTIVITY_TYPE_HOME) { mHomeBuilderProxy.onBuildTargetParams(builder, app, this); } else { // Fade out Assistant overlay. - if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT - && app.isNotInRecents) { + if (activityType == ACTIVITY_TYPE_ASSISTANT && app.isNotInRecents) { float progress = Utilities.boundToRange(getProgress(), 0, 1); - builder.withAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress)); + builder.setAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress)); } else { - builder.withAlpha(getTargetAlpha()); + builder.setAlpha(getTargetAlpha()); } proxy.onBuildTargetParams(builder, app, this); @@ -164,20 +165,26 @@ public class TransformParams { } else { mBaseBuilderProxy.onBuildTargetParams(builder, app, this); } - surfaceParams[i] = builder.build(); } - return surfaceParams; + + // always put wallpaper layer to bottom. + final int wallpaperLength = targets.wallpapers != null ? targets.wallpapers.length : 0; + for (int i = 0; i < wallpaperLength; i++) { + RemoteAnimationTarget wallpaper = targets.wallpapers[i]; + transaction.forSurface(wallpaper.leash).setLayer(Integer.MIN_VALUE); + } + return transaction; } private static SurfaceControl getRecentsSurface(RemoteAnimationTargets targets) { for (int i = 0; i < targets.unfilteredApps.length; i++) { - RemoteAnimationTargetCompat app = targets.unfilteredApps[i]; + RemoteAnimationTarget app = targets.unfilteredApps[i]; if (app.mode == targets.targetMode) { - if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS) { - return app.leash.getSurfaceControl(); + if (app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_RECENTS) { + return app.leash; } } else { - return app.leash.getSurfaceControl(); + return app.leash; } } return null; @@ -205,15 +212,11 @@ public class TransformParams { return mTargetSet; } - public void applySurfaceParams(SurfaceParams... params) { + public void applySurfaceParams(SurfaceTransaction builder) { if (mSyncTransactionApplier != null) { - mSyncTransactionApplier.scheduleApply(params); + mSyncTransactionApplier.scheduleApply(builder); } else { - TransactionCompat t = new TransactionCompat(); - for (SurfaceParams param : params) { - SyncRtSurfaceTransactionApplierCompat.applyParams(t, param); - } - t.apply(); + builder.getTransaction().apply(); } } @@ -221,9 +224,9 @@ public class TransformParams { public interface BuilderProxy { BuilderProxy NO_OP = (builder, app, params) -> { }; - BuilderProxy ALWAYS_VISIBLE = (builder, app, params) ->builder.withAlpha(1); + BuilderProxy ALWAYS_VISIBLE = (builder, app, params) -> builder.setAlpha(1); - void onBuildTargetParams(SurfaceParams.Builder builder, - RemoteAnimationTargetCompat app, TransformParams params); + void onBuildTargetParams(SurfaceProperties builder, + RemoteAnimationTarget app, TransformParams params); } } diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java index dc97dd6f86..973053dec3 100644 --- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java +++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java @@ -21,6 +21,7 @@ import android.view.WindowManager; import com.android.launcher3.Hotseat; import com.android.launcher3.Launcher; +import com.android.systemui.unfold.updates.RotationChangeProvider; /** * Animation that moves hotseat icons from center to the sides (final position) @@ -29,8 +30,9 @@ public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCente private final Launcher mLauncher; - public UnfoldMoveFromCenterHotseatAnimator(Launcher launcher, WindowManager windowManager) { - super(windowManager); + public UnfoldMoveFromCenterHotseatAnimator(Launcher launcher, WindowManager windowManager, + RotationChangeProvider rotationChangeProvider) { + super(windowManager, rotationChangeProvider); mLauncher = launcher; } @@ -39,7 +41,8 @@ public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCente Hotseat hotseat = mLauncher.getHotseat(); ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets(); - disableClipping(hotseat); + setClipChildren(hotseat, false); + setClipToPadding(hotseat, false); for (int i = 0; i < hotseatIcons.getChildCount(); i++) { View child = hotseatIcons.getChildAt(i); @@ -49,7 +52,12 @@ public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCente @Override public void onTransitionFinished() { - restoreClipping(mLauncher.getHotseat()); + restoreClippings(); super.onTransitionFinished(); } + + @Override + public void onTransitionFinishing() { + + } } diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java index 3d72398832..c86ca80b85 100644 --- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java +++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java @@ -22,6 +22,7 @@ import com.android.launcher3.CellLayout; import com.android.launcher3.Launcher; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Workspace; +import com.android.systemui.unfold.updates.RotationChangeProvider; /** * Animation that moves launcher icons and widgets from center to the sides (final position) @@ -30,14 +31,15 @@ public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCen private final Launcher mLauncher; - public UnfoldMoveFromCenterWorkspaceAnimator(Launcher launcher, WindowManager windowManager) { - super(windowManager); + public UnfoldMoveFromCenterWorkspaceAnimator(Launcher launcher, WindowManager windowManager, + RotationChangeProvider rotationChangeProvider) { + super(windowManager, rotationChangeProvider); mLauncher = launcher; } @Override protected void onPrepareViewsForAnimation() { - Workspace workspace = mLauncher.getWorkspace(); + Workspace workspace = mLauncher.getWorkspace(); // App icons and widgets workspace @@ -45,7 +47,8 @@ public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCen final CellLayout cellLayout = (CellLayout) page; ShortcutAndWidgetContainer itemsContainer = cellLayout .getShortcutsAndWidgets(); - disableClipping(cellLayout); + setClipChildren(cellLayout, false); + setClipToPadding(cellLayout, false); for (int i = 0; i < itemsContainer.getChildCount(); i++) { View child = itemsContainer.getChildAt(i); @@ -53,13 +56,18 @@ public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCen } }); - disableClipping(workspace); + setClipChildren(workspace, false); + setClipToPadding(workspace, true); } @Override public void onTransitionFinished() { - restoreClipping(mLauncher.getWorkspace()); - mLauncher.getWorkspace().forEachVisiblePage(page -> restoreClipping((CellLayout) page)); + restoreClippings(); super.onTransitionFinished(); } + + @Override + public void onTransitionFinishing() { + + } } diff --git a/quickstep/src/com/android/quickstep/util/VibrationConstants.java b/quickstep/src/com/android/quickstep/util/VibrationConstants.java new file mode 100644 index 0000000000..0f0306e344 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/VibrationConstants.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.os.VibrationEffect; + +public class VibrationConstants { + public static final VibrationEffect EFFECT_TEXTURE_TICK = + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TEXTURE_TICK); +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/util/VibratorWrapper.java b/quickstep/src/com/android/quickstep/util/VibratorWrapper.java deleted file mode 100644 index abf9e162e4..0000000000 --- a/quickstep/src/com/android/quickstep/util/VibratorWrapper.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; - -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.media.AudioAttributes; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.provider.Settings; - -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MainThreadInitializedObject; - -/** - * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. - */ -@TargetApi(Build.VERSION_CODES.Q) -public class VibratorWrapper { - - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(VibratorWrapper::new); - - public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - - public static final VibrationEffect EFFECT_CLICK = - createPredefined(VibrationEffect.EFFECT_CLICK); - public static final VibrationEffect EFFECT_TEXTURE_TICK = - createPredefined(VibrationEffect.EFFECT_TEXTURE_TICK); - - /** - * Haptic when entering overview. - */ - public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; - - private final Vibrator mVibrator; - private final boolean mHasVibrator; - - private boolean mIsHapticFeedbackEnabled; - - public VibratorWrapper(Context context) { - mVibrator = context.getSystemService(Vibrator.class); - mHasVibrator = mVibrator.hasVibrator(); - if (mHasVibrator) { - final ContentResolver resolver = context.getContentResolver(); - mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver); - final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) { - @Override - public void onChange(boolean selfChange) { - mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver); - } - }; - resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED), - false /* notifyForDescendents */, observer); - } else { - mIsHapticFeedbackEnabled = false; - } - } - - private boolean isHapticFeedbackEnabled(ContentResolver resolver) { - return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1; - } - - /** Vibrates with the given effect if haptic feedback is available and enabled. */ - public void vibrate(VibrationEffect vibrationEffect) { - if (mHasVibrator && mIsHapticFeedbackEnabled) { - UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect, VIBRATION_ATTRS)); - } - } - - /** - * Vibrates with a single primitive, if supported, or use a fallback effect instead. This only - * vibrates if haptic feedback is available and enabled. - */ - @SuppressLint("NewApi") - public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { - if (mHasVibrator && mIsHapticFeedbackEnabled) { - UI_HELPER_EXECUTOR.execute(() -> { - if (Utilities.ATLEAST_R && primitiveId >= 0 - && mVibrator.areAllPrimitivesSupported(primitiveId)) { - mVibrator.vibrate(VibrationEffect.startComposition() - .addPrimitive(primitiveId, primitiveScale) - .compose(), VIBRATION_ATTRS); - } else { - mVibrator.vibrate(fallbackEffect, VIBRATION_ATTRS); - } - }); - } - } - - private static VibrationEffect createPredefined(int effectId) { - if (!Utilities.ATLEAST_Q) return null; - return VibrationEffect.createPredefined(effectId); - } -} diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java index 7ae6cb7661..34fa7f1764 100644 --- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java +++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java @@ -15,7 +15,9 @@ */ package com.android.quickstep.util; -import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY; +import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_REVEAL_ANIM; +import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; @@ -27,9 +29,10 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.util.FloatProperty; import android.view.View; -import com.android.launcher3.BaseQuickstepLauncher; +import com.android.launcher3.Hotseat; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Workspace; @@ -37,6 +40,7 @@ import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DynamicResource; import com.android.quickstep.views.RecentsView; import com.android.systemui.plugins.ResourceProvider; @@ -49,6 +53,11 @@ public class WorkspaceRevealAnim { // Should be used for animations running alongside this WorkspaceRevealAnim. public static final int DURATION_MS = 350; + private static final FloatProperty> WORKSPACE_SCALE_PROPERTY = + WORKSPACE_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_REVEAL_ANIM); + + private static final FloatProperty HOTSEAT_SCALE_PROPERTY = + HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_REVEAL_ANIM); private final float mScaleStart; private final AnimatorSet mAnimators = new AnimatorSet(); @@ -59,12 +68,12 @@ public class WorkspaceRevealAnim { ResourceProvider rp = DynamicResource.provider(launcher); mScaleStart = rp.getFloat(R.dimen.swipe_up_scale_start); - Workspace workspace = launcher.getWorkspace(); + Workspace workspace = launcher.getWorkspace(); workspace.setPivotToScaleWithSelf(launcher.getHotseat()); // Add reveal animations. - addRevealAnimatorsForView(workspace); - addRevealAnimatorsForView(launcher.getHotseat()); + addRevealAnimatorsForView(workspace, WORKSPACE_SCALE_PROPERTY); + addRevealAnimatorsForView(launcher.getHotseat(), HOTSEAT_SCALE_PROPERTY); // Add overview scrim animation. if (animateOverviewScrim) { @@ -75,9 +84,9 @@ public class WorkspaceRevealAnim { } // Add depth controller animation. - if (launcher instanceof BaseQuickstepLauncher) { + if (launcher instanceof QuickstepLauncher) { PendingAnimation depthBuilder = new PendingAnimation(DURATION_MS); - DepthController depth = ((BaseQuickstepLauncher) launcher).getDepthController(); + DepthController depth = ((QuickstepLauncher) launcher).getDepthController(); depth.setStateWithAnimation(NORMAL, new StateAnimationConfig(), depthBuilder); mAnimators.play(depthBuilder.buildAnim()); } @@ -89,8 +98,8 @@ public class WorkspaceRevealAnim { mAnimators.setInterpolator(Interpolators.DECELERATED_EASE); } - private void addRevealAnimatorsForView(View v) { - ObjectAnimator scale = ObjectAnimator.ofFloat(v, SCALE_PROPERTY, mScaleStart, 1f); + private void addRevealAnimatorsForView(T v, FloatProperty scaleProperty) { + ObjectAnimator scale = ObjectAnimator.ofFloat(v, scaleProperty, mScaleStart, 1f); scale.setDuration(DURATION_MS); scale.setInterpolator(Interpolators.DECELERATED_EASE); mAnimators.play(scale); @@ -103,7 +112,7 @@ public class WorkspaceRevealAnim { mAnimators.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - SCALE_PROPERTY.set(v, 1f); + scaleProperty.set(v, 1f); v.setAlpha(1f); } }); diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java index d79b318023..716d389a2c 100644 --- a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java +++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java @@ -111,11 +111,6 @@ public class AllAppsEduView extends AbstractFloatingView { return (type & TYPE_ALL_APPS_EDU) != 0; } - @Override - public boolean onBackPressed() { - return true; - } - @Override public boolean canInterceptEventsInSystemGestureRegion() { return true; diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java index f20bbc50b7..34dc3a1737 100644 --- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java +++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java @@ -70,7 +70,6 @@ public class ClearAllButton extends Button { private float mGridTranslationPrimary; private float mGridScrollOffset; private float mScrollOffsetPrimary; - private float mSplitSelectScrollOffsetPrimary; private int mSidePadding; @@ -104,6 +103,10 @@ public class ClearAllButton extends Button { return false; } + public float getScrollAlpha() { + return mScrollAlpha; + } + public void setContentAlpha(float alpha) { if (mContentAlpha != alpha) { mContentAlpha = alpha; @@ -146,7 +149,10 @@ public class ClearAllButton extends Button { } applyPrimaryTranslation(); applySecondaryTranslation(); - mScrollAlpha = 1 - shift / orientationSize; + float clearAllSpacing = + recentsView.getPageSpacing() + recentsView.getClearAllExtraPageSpacing(); + clearAllSpacing = mIsRtl ? -clearAllSpacing : clearAllSpacing; + mScrollAlpha = Math.max((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing, 0); updateAlpha(); } @@ -174,10 +180,6 @@ public class ClearAllButton extends Button { mScrollOffsetPrimary = scrollOffsetPrimary; } - public void setSplitSelectScrollOffsetPrimary(float splitSelectScrollOffsetPrimary) { - mSplitSelectScrollOffsetPrimary = splitSelectScrollOffsetPrimary; - } - public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) { float scrollAdjustment = 0; if (fullscreenEnabled) { @@ -187,7 +189,6 @@ public class ClearAllButton extends Button { scrollAdjustment += mGridTranslationPrimary + mGridScrollOffset; } scrollAdjustment += mScrollOffsetPrimary; - scrollAdjustment += mSplitSelectScrollOffsetPrimary; return scrollAdjustment; } @@ -252,7 +253,7 @@ public class ClearAllButton extends Button { */ private float getOriginalTranslationY() { DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - return deviceProfile.overviewShowAsGrid + return deviceProfile.isTablet ? deviceProfile.overviewRowSpacing : deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f; } diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java new file mode 100644 index 0000000000..ccc2df61c9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.os.SystemProperties; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +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.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.RunnableList; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TaskThumbnailCache; +import com.android.quickstep.util.CancellableTask; +import com.android.quickstep.util.RecentsOrientedState; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +/** + * TaskView that contains all tasks that are part of the desktop. + */ +// TODO(b/249371338): TaskView needs to be refactored to have better support for N tasks. +public class DesktopTaskView extends TaskView { + + /** Flag to indicate whether desktop windowing proto 1 is enabled */ + private static final boolean DESKTOP_IS_PROTO1_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode", false); + + /** Flag to indicate whether desktop windowing proto 2 is enabled */ + public static final boolean DESKTOP_IS_PROTO2_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode_2", false); + + /** Flags to indicate whether desktop mode is available on the device */ + public static final boolean DESKTOP_MODE_SUPPORTED = + DESKTOP_IS_PROTO1_ENABLED || DESKTOP_IS_PROTO2_ENABLED; + + private static final String TAG = DesktopTaskView.class.getSimpleName(); + + private static final boolean DEBUG = true; + + @NonNull + private List mTasks = new ArrayList<>(); + + private final ArrayList mSnapshotViews = new ArrayList<>(); + + /** Maps {@code taskIds} to corresponding {@link TaskThumbnailView}s */ + private final SparseArray mSnapshotViewMap = new SparseArray<>(); + + private final ArrayList> mPendingThumbnailRequests = new ArrayList<>(); + + private View mBackgroundView; + + public DesktopTaskView(Context context) { + this(context, null); + } + + public DesktopTaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mBackgroundView = findViewById(R.id.background); + + int topMarginPx = + mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx; + FrameLayout.LayoutParams params = (LayoutParams) mBackgroundView.getLayoutParams(); + params.topMargin = topMarginPx; + mBackgroundView.setLayoutParams(params); + + float[] outerRadii = new float[8]; + Arrays.fill(outerRadii, getTaskCornerRadius()); + RoundRectShape shape = new RoundRectShape(outerRadii, null, null); + ShapeDrawable background = new ShapeDrawable(shape); + background.setTint(getResources().getColor(android.R.color.system_neutral2_300, + getContext().getTheme())); + // TODO(b/244348395): this should be wallpaper + mBackgroundView.setBackground(background); + + Drawable icon = getResources().getDrawable(R.drawable.ic_desktop, getContext().getTheme()); + Drawable iconBackground = getResources().getDrawable(R.drawable.bg_circle, + getContext().getTheme()); + mIconView.setDrawable(new LayerDrawable(new Drawable[]{iconBackground, icon})); + } + + @Override + protected void updateBorderBounds(Rect bounds) { + bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(), + mBackgroundView.getBottom()); + } + + @Override + public void bind(Task task, RecentsOrientedState orientedState) { + bind(Collections.singletonList(task), orientedState); + } + + /** + * Updates this desktop task to the gives task list defined in {@code tasks} + */ + public void bind(List tasks, RecentsOrientedState orientedState) { + if (DEBUG) { + StringBuilder sb = new StringBuilder(); + sb.append("bind tasks=").append(tasks.size()).append("\n"); + for (Task task : tasks) { + sb.append(" key=").append(task.key).append("\n"); + } + Log.d(TAG, sb.toString()); + } + cancelPendingLoadTasks(); + + mTasks = new ArrayList<>(tasks); + mSnapshotViewMap.clear(); + + // Ensure there are equal number of snapshot views and tasks. + // More tasks than views, add views. More views than tasks, remove views. + // TODO(b/251586230): use a ViewPool for creating TaskThumbnailViews + if (mSnapshotViews.size() > mTasks.size()) { + int diff = mSnapshotViews.size() - mTasks.size(); + for (int i = 0; i < diff; i++) { + TaskThumbnailView snapshotView = mSnapshotViews.remove(0); + removeView(snapshotView); + } + } else if (mSnapshotViews.size() < mTasks.size()) { + int diff = mTasks.size() - mSnapshotViews.size(); + for (int i = 0; i < diff; i++) { + TaskThumbnailView snapshotView = new TaskThumbnailView(getContext()); + mSnapshotViews.add(snapshotView); + addView(snapshotView, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + } + + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + TaskThumbnailView snapshotView = mSnapshotViews.get(i); + snapshotView.bind(task); + mSnapshotViewMap.put(task.key.id, snapshotView); + } + + updateTaskIdContainer(); + updateTaskIdAttributeContainer(); + + setOrientationState(orientedState); + } + + private void updateTaskIdContainer() { + // TODO(b/249371338): TaskView expects the array to have at least 2 elements. + // At least 2 elements in the array + mTaskIdContainer = new int[Math.max(mTasks.size(), 2)]; + for (int i = 0; i < mTasks.size(); i++) { + mTaskIdContainer[i] = mTasks.get(i).key.id; + } + } + + private void updateTaskIdAttributeContainer() { + // TODO(b/249371338): TaskView expects the array to have at least 2 elements. + // At least 2 elements in the array + mTaskIdAttributeContainer = new TaskIdAttributeContainer[Math.max(mTasks.size(), 2)]; + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + mTaskIdAttributeContainer[i] = createAttributeContainer(task, thumbnailView); + } + } + + private TaskIdAttributeContainer createAttributeContainer(Task task, + TaskThumbnailView thumbnailView) { + return new TaskIdAttributeContainer(task, thumbnailView, null, STAGE_POSITION_UNDEFINED); + } + + @Nullable + @Override + public Task getTask() { + // TODO(b/249371338): returning first task. This won't work well with multiple tasks. + return mTasks.size() > 0 ? mTasks.get(0) : null; + } + + @Override + public TaskThumbnailView getThumbnail() { + // TODO(b/249371338): returning single thumbnail. This won't work well with multiple tasks. + Task task = getTask(); + if (task != null) { + return mSnapshotViewMap.get(task.key.id); + } + // Return the place holder snapshot views. Callers expect this to be non-null + return mSnapshotView; + } + + @Override + public boolean containsTaskId(int taskId) { + // Thumbnail map contains taskId -> thumbnail map. Use the keys for contains + return mSnapshotViewMap.contains(taskId); + } + + @Override + public void onTaskListVisibilityChanged(boolean visible, int changes) { + cancelPendingLoadTasks(); + if (visible) { + RecentsModel model = RecentsModel.INSTANCE.get(getContext()); + TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); + + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + for (Task task : mTasks) { + CancellableTask thumbLoadRequest = + thumbnailCache.updateThumbnailInBackground(task, thumbnailData -> { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(task, thumbnailData); + } + }); + if (thumbLoadRequest != null) { + mPendingThumbnailRequests.add(thumbLoadRequest); + } + } + } + } else { + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + for (Task task : mTasks) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(null, null); + } + // Reset the task thumbnail ref + task.thumbnail = null; + } + } + } + } + + @Override + protected void setThumbnailOrientation(RecentsOrientedState orientationState) { + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); + int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + + LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); + snapshotParams.topMargin = thumbnailTopMargin; + + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i); + thumbnailView.setLayoutParams(snapshotParams); + } + } + + @Override + protected void cancelPendingLoadTasks() { + for (CancellableTask cancellableTask : mPendingThumbnailRequests) { + cancellableTask.cancel(); + } + mPendingThumbnailRequests.clear(); + } + + @Override + public boolean offerTouchToChildren(MotionEvent event) { + return false; + } + + @Override + protected boolean showTaskMenuWithContainer(IconView iconView) { + return false; + } + + @Override + public RunnableList launchTasks() { + SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps(); + Launcher.getLauncher(mActivity).getStateManager().goToState(NORMAL, false /* animated */); + return null; + } + + @Nullable + @Override + public RunnableList launchTaskAnimated() { + return launchTasks(); + } + + @Override + public void launchTask(@NonNull Consumer callback, boolean freezeTaskList) { + launchTasks(); + callback.accept(true); + } + + @Override + public boolean isDesktopTask() { + return true; + } + + @Override + void refreshThumbnails(@Nullable HashMap thumbnailDatas) { + // Sets new thumbnails based on the incoming data and refreshes the rest. + // Create a copy of the thumbnail map, so we can track thumbnails that need refreshing. + SparseArray thumbnailsToRefresh = mSnapshotViewMap.clone(); + if (thumbnailDatas != null) { + for (Task task : mTasks) { + int key = task.key.id; + TaskThumbnailView thumbnailView = thumbnailsToRefresh.get(key); + ThumbnailData thumbnailData = thumbnailDatas.get(key); + if (thumbnailView != null && thumbnailData != null) { + thumbnailView.setThumbnail(task, thumbnailData); + // Remove this thumbnail from the list that should be refreshed. + thumbnailsToRefresh.remove(key); + } + } + } + + // Refresh the rest that were not updated. + for (int i = 0; i < thumbnailsToRefresh.size(); i++) { + thumbnailsToRefresh.valueAt(i).refresh(); + } + } + + @Override + public TaskThumbnailView[] getThumbnails() { + TaskThumbnailView[] thumbnails = new TaskThumbnailView[mSnapshotViewMap.size()]; + for (int i = 0; i < thumbnails.length; i++) { + thumbnails[i] = mSnapshotViewMap.valueAt(i); + } + return thumbnails; + } + + @Override + public void onRecycle() { + resetPersistentViewTransforms(); + // Clear any references to the thumbnail (it will be re-read either from the cache or the + // system on next bind) + for (Task task : mTasks) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(task, null); + } + } + setOverlayEnabled(false); + onTaskListVisibilityChanged(false); + setVisibility(VISIBLE); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int containerWidth = MeasureSpec.getSize(widthMeasureSpec); + int containerHeight = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(containerWidth, containerHeight); + + int thumbnailTopMarginPx = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx; + containerHeight -= thumbnailTopMarginPx; + + int thumbnails = mSnapshotViewMap.size(); + if (thumbnails == 0) { + return; + } + + int windowWidth = mActivity.getDeviceProfile().widthPx; + int windowHeight = mActivity.getDeviceProfile().heightPx; + + float scaleWidth = containerWidth / (float) windowWidth; + float scaleHeight = containerHeight / (float) windowHeight; + + 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. + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + Rect taskSize = task.appBounds; + if (taskSize == null) { + // Default to quarter of the desktop if we did not get app bounds. + taskSize = new Rect(0, 0, windowWidth / 4, windowHeight / 4); + } + + int thumbWidth = (int) (taskSize.width() * scaleWidth); + int thumbHeight = (int) (taskSize.height() * scaleHeight); + + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.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 + Point positionInParent = task.positionInParent; + if (positionInParent == null) { + positionInParent = new Point(0, 0); + } + int taskX = (int) (positionInParent.x * scaleWidth); + int taskY = (int) (positionInParent.y * scaleHeight); + // move task down by margin size + taskY += thumbnailTopMarginPx; + thumbnailView.setX(taskX); + thumbnailView.setY(taskY); + + if (DEBUG) { + Log.d(TAG, "onMeasure: task=" + task.key + " thumb=[" + thumbWidth + "," + + thumbHeight + "]" + " pos=[" + taskX + "," + taskY + "]"); + } + } + } + } + + @Override + public void setOverlayEnabled(boolean overlayEnabled) { + // Intentional no-op to prevent setting smart actions overlay on thumbnails + } + + @Override + public void setFullscreenProgress(float progress) { + // TODO(b/249371338): this copies parent implementation and makes it work for N thumbs + progress = Utilities.boundToRange(progress, 0, 1); + mFullscreenProgress = progress; + if (mFullscreenProgress > 0) { + // Don't show background while we are transitioning to/from fullscreen + mBackgroundView.setVisibility(INVISIBLE); + } else { + mBackgroundView.setVisibility(VISIBLE); + } + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i); + thumbnailView.getTaskOverlay().setFullscreenProgress(progress); + updateSnapshotRadius(); + } + } + + @Override + protected void updateSnapshotRadius() { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams); + } + } + + @Override + protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { + // no-op + } + + @Override + public void setColorTint(float amount, int tintColor) { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setDimAlpha(amount); + } + } + + @Override + protected void applyThumbnailSplashAlpha() { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setSplashAlpha(mTaskThumbnailSplashAlpha); + } + } + + @Override + void setThumbnailVisibility(int visibility, int taskId) { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setVisibility(visibility); + } + } + + @Override + protected boolean confirmSecondSplitSelectApp() { + // Desktop tile can't be in split screen + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index e5664c6032..7cd6756362 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -17,9 +17,6 @@ package com.android.quickstep.views; import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS; -import static android.view.Gravity.BOTTOM; -import static android.view.Gravity.CENTER_HORIZONTAL; -import static android.view.Gravity.START; import static com.android.launcher3.Utilities.prefixTextWithIcon; import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR; @@ -56,7 +53,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.touch.PagedOrientationHandler; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.systemui.shared.recents.model.Task; import java.lang.annotation.Retention; @@ -100,13 +97,8 @@ public final class DigitalWellBeingToast { private View mBanner; private ViewOutlineProvider mOldBannerOutlineProvider; private float mBannerOffsetPercentage; - /** - * Clips rect provided by {@link #mOldBannerOutlineProvider} when in the model state to - * hide this banner as the taskView scales up and down - */ - private float mModalOffset = 0f; @Nullable - private StagedSplitBounds mStagedSplitBounds; + private SplitBounds mSplitBounds; private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; private float mSplitOffsetTranslationY; private float mSplitOffsetTranslationX; @@ -147,12 +139,6 @@ public final class DigitalWellBeingToast { public void initialize(Task task) { mTask = task; - - if (task.key.userId != UserHandle.myUserId()) { - setNoLimit(); - return; - } - THREAD_POOL_EXECUTOR.execute(() -> { final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit( task.getTopComponent().getPackageName(), @@ -173,11 +159,11 @@ public final class DigitalWellBeingToast { }); } - public void setSplitConfiguration(StagedSplitBounds stagedSplitBounds) { - mStagedSplitBounds = stagedSplitBounds; - if (mStagedSplitBounds == null || - !mActivity.getDeviceProfile().overviewShowAsGrid || - mTaskView.isFocusedTask()) { + public void setSplitConfiguration(SplitBounds splitBounds) { + mSplitBounds = splitBounds; + if (mSplitBounds == null + || !mActivity.getDeviceProfile().isTablet + || mTaskView.isFocusedTask()) { mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; return; } @@ -189,11 +175,11 @@ public final class DigitalWellBeingToast { } // For landscape grid, for 30% width we only show icon, otherwise show icon and time - if (mTask.key.id == mStagedSplitBounds.leftTopTaskId) { - mSplitBannerConfig = mStagedSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? + if (mTask.key.id == mSplitBounds.leftTopTaskId) { + mSplitBannerConfig = mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } else { - mSplitBannerConfig = mStagedSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? + mSplitBannerConfig = mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } } @@ -315,7 +301,7 @@ public final class DigitalWellBeingToast { private void setBanner(@Nullable View view) { mBanner = view; - if (view != null) { + if (view != null && mTaskView.getRecentsView() != null) { setupAndAddBanner(); setBannerOutline(); } @@ -329,8 +315,8 @@ public final class DigitalWellBeingToast { mTaskView.getThumbnail().getLayoutParams()).bottomMargin; PagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler(); Pair translations = orientationHandler - .setDwbLayoutParamsAndGetTranslations(mTaskView.getMeasuredWidth(), - mTaskView.getMeasuredHeight(), mStagedSplitBounds, deviceProfile, + .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(), + mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile, mTaskView.getThumbnails(), mTask.key.id, mBanner); mSplitOffsetTranslationX = translations.first; mSplitOffsetTranslationY = translations.second; @@ -340,22 +326,24 @@ public final class DigitalWellBeingToast { } private void setBannerOutline() { - mOldBannerOutlineProvider = mBanner.getOutlineProvider(); + // 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() + mModalOffset - + mSplitOffsetTranslationY; + float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY; outline.offset(0, Math.round(verticalTranslation)); } }); mBanner.setClipToOutline(true); } - void updateBannerOffset(float offsetPercentage, float verticalOffset) { + void updateBannerOffset(float offsetPercentage) { if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) { - mModalOffset = verticalOffset; mBannerOffsetPercentage = offsetPercentage; updateTranslationY(); mBanner.invalidateOutline(); @@ -368,10 +356,7 @@ public final class DigitalWellBeingToast { } mBanner.setTranslationY( - (mBannerOffsetPercentage * mBanner.getHeight()) + - mModalOffset + - mSplitOffsetTranslationY - ); + (mBannerOffsetPercentage * mBanner.getHeight()) + mSplitOffsetTranslationY); } private void updateTranslationX() { @@ -394,4 +379,12 @@ public final class DigitalWellBeingToast { mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint); mBanner.setLayerPaint(layerPaint); } + + void setBannerVisibility(int visibility) { + if (mBanner == null) { + return; + } + + mBanner.setVisibility(visibility); + } } diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskThumbnailView.java new file mode 100644 index 0000000000..d869fed37e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/FloatingTaskThumbnailView.java @@ -0,0 +1,80 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; + +/** + * A child view of {@link com.android.quickstep.views.FloatingTaskView} to draw the thumbnail in a + * rounded corner frame. While the purpose of this class sounds similar to + * {@link TaskThumbnailView}, it doesn't need a lot of complex logic in {@link TaskThumbnailView} + * in relation to moving with {@link RecentsView}. + */ +public class FloatingTaskThumbnailView extends View { + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Matrix mMatrix = new Matrix(); + + private @Nullable BitmapShader mBitmapShader; + private @Nullable Bitmap mBitmap; + + public FloatingTaskThumbnailView(Context context) { + this(context, null); + } + + public FloatingTaskThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FloatingTaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap == null) { + return; + } + + // Scale down the bitmap to fix x, and crop in y. + float scale = 1.0f * getMeasuredWidth() / mBitmap.getWidth(); + mMatrix.reset(); + mMatrix.postScale(scale, scale); + mBitmapShader.setLocalMatrix(mMatrix); + + FloatingTaskView parent = (FloatingTaskView) getParent(); + parent.drawRoundedRect(canvas, mPaint); + } + + public void setThumbnail(Bitmap bitmap) { + mBitmap = bitmap; + if (bitmap != null) { + mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mPaint.setShader(mBitmapShader); + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java index 18ab3bb2e9..75a8ea2b3b 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java @@ -1,40 +1,51 @@ package com.android.quickstep.views; -import static com.android.launcher3.anim.Interpolators.ACCEL; -import static com.android.launcher3.anim.Interpolators.DEACCEL_3; +import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; +import static com.android.launcher3.anim.Interpolators.clampToProgress; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.util.FloatProperty; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.ImageView; import androidx.annotation.Nullable; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseActivity; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.InsettableFrameLayout; -import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.touch.PagedOrientationHandler; +import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.SplitAnimationTimings; +import com.android.quickstep.util.TaskCornerRadius; +import com.android.systemui.shared.system.QuickStepContract; /** - * Create an instance via {@link #getFloatingTaskView(StatefulActivity, TaskView, RectF)} to + * Create an instance via + * {@link #getFloatingTaskView(StatefulActivity, View, Bitmap, Drawable, RectF)} to * which will have the thumbnail from the provided existing TaskView overlaying the taskview itself. * * Can then animate the taskview using - * {@link #addAnimation(PendingAnimation, RectF, Rect, View, boolean)} + * {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} or + * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} * giving a starting and ending bounds. Currently this is set to use the split placeholder view, * but it could be generified. * @@ -42,13 +53,40 @@ import com.android.quickstep.util.MultiValueUpdateListener; */ public class FloatingTaskView extends FrameLayout { + public static final FloatProperty PRIMARY_TRANSLATE_OFFSCREEN = + new FloatProperty("floatingTaskPrimaryTranslateOffscreen") { + @Override + public void setValue(FloatingTaskView view, float translation) { + ((RecentsView) view.mActivity.getOverviewPanel()).getPagedOrientationHandler() + .setFloatingTaskPrimaryTranslation( + view, + translation, + view.mActivity.getDeviceProfile() + ); + } + + @Override + public Float get(FloatingTaskView view) { + return ((RecentsView) view.mActivity.getOverviewPanel()) + .getPagedOrientationHandler() + .getFloatingTaskPrimaryTranslation( + view, + view.mActivity.getDeviceProfile() + ); + } + }; + + private int mSplitHolderSize; + private FloatingTaskThumbnailView mThumbnailView; private SplitPlaceholderView mSplitPlaceholderView; private RectF mStartingPosition; - private final BaseDraggingActivity mActivity; + private final StatefulActivity mActivity; private final boolean mIsRtl; - private final Rect mOutline = new Rect(); + private final FullscreenDrawParams mFullscreenParams; private PagedOrientationHandler mOrientationHandler; - private ImageView mImageView; + @SplitConfigurationOptions.StagePosition + private int mStagePosition; + private final Rect mTmpRect = new Rect(); public FloatingTaskView(Context context) { this(context, null); @@ -62,37 +100,40 @@ public class FloatingTaskView extends FrameLayout { super(context, attrs, defStyleAttr); mActivity = BaseActivity.fromContext(context); mIsRtl = Utilities.isRtl(getResources()); + mFullscreenParams = new FullscreenDrawParams(context); + + mSplitHolderSize = context.getResources().getDimensionPixelSize( + R.dimen.split_placeholder_icon_size); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mImageView = findViewById(R.id.thumbnail); - mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - mImageView.setLayerType(LAYER_TYPE_HARDWARE, null); + mThumbnailView = findViewById(R.id.thumbnail); mSplitPlaceholderView = findViewById(R.id.split_placeholder); mSplitPlaceholderView.setAlpha(0); } - private void init(StatefulActivity launcher, TaskView originalView, RectF positionOut) { + private void init(StatefulActivity launcher, View originalView, @Nullable Bitmap thumbnail, + Drawable icon, RectF positionOut) { mStartingPosition = positionOut; updateInitialPositionForView(originalView); final InsettableFrameLayout.LayoutParams lp = (InsettableFrameLayout.LayoutParams) getLayoutParams(); mSplitPlaceholderView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height)); - positionOut.round(mOutline); setPivotX(0); setPivotY(0); // Copy bounds of exiting thumbnail into ImageView - TaskThumbnailView thumbnail = originalView.getThumbnail(); - mImageView.setImageBitmap(thumbnail.getThumbnail()); - mImageView.setVisibility(VISIBLE); + mThumbnailView.setThumbnail(thumbnail); - mOrientationHandler = originalView.getRecentsView().getPagedOrientationHandler(); - mSplitPlaceholderView.setIconView(originalView.getIconView(), - launcher.getDeviceProfile().overviewTaskIconDrawableSizePx); + mThumbnailView.setVisibility(VISIBLE); + + RecentsView recentsView = launcher.getOverviewPanel(); + mOrientationHandler = recentsView.getPagedOrientationHandler(); + mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition(); + mSplitPlaceholderView.setIcon(icon, mSplitHolderSize); mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); } @@ -101,52 +142,68 @@ public class FloatingTaskView extends FrameLayout { * appearance of {@code originalView}. */ public static FloatingTaskView getFloatingTaskView(StatefulActivity launcher, - TaskView originalView, RectF positionOut) { - final BaseDragLayer dragLayer = launcher.getDragLayer(); - ViewGroup parent = (ViewGroup) dragLayer.getParent(); + View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut) { + final ViewGroup dragLayer = launcher.getDragLayer(); final FloatingTaskView floatingView = (FloatingTaskView) launcher.getLayoutInflater() - .inflate(R.layout.floating_split_select_view, parent, false); + .inflate(R.layout.floating_split_select_view, dragLayer, false); - floatingView.init(launcher, originalView, positionOut); - parent.addView(floatingView); + floatingView.init(launcher, originalView, thumbnail, icon, positionOut); + // Add this animating view underneath the existing open task menu view (if there is one) + View openTaskView = AbstractFloatingView.getOpenView(launcher, TYPE_TASK_MENU); + int openTaskViewIndex = dragLayer.indexOfChild(openTaskView); + if (openTaskViewIndex == -1) { + // Add to top if not + openTaskViewIndex = dragLayer.getChildCount(); + } + dragLayer.addView(floatingView, openTaskViewIndex - 1); return floatingView; } - public void updateInitialPositionForView(TaskView originalView) { - View thumbnail = originalView.getThumbnail(); - Rect viewBounds = new Rect(0, 0, thumbnail.getWidth(), thumbnail.getHeight()); - Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), thumbnail, viewBounds, - true /* ignoreTransform */, null /* recycle */, - mStartingPosition); - mStartingPosition.offset(originalView.getTranslationX(), originalView.getTranslationY()); - final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams( + public void updateInitialPositionForView(View originalView) { + if (originalView.getContext() instanceof TaskbarActivityContext) { + // If original View is a button on the Taskbar, find the on-screen bounds and calculate + // the equivalent bounds in the DragLayer, so we can set the initial position of + // this FloatingTaskView and start the split animation at the correct spot. + originalView.getBoundsOnScreen(mTmpRect); + mStartingPosition.set(mTmpRect); + int[] dragLayerPositionRelativeToScreen = + mActivity.getDragLayer().getLocationOnScreen(); + mStartingPosition.offset( + -dragLayerPositionRelativeToScreen[0], + -dragLayerPositionRelativeToScreen[1]); + } else { + Rect viewBounds = new Rect(0, 0, originalView.getWidth(), originalView.getHeight()); + Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), originalView, + viewBounds, false /* ignoreTransform */, null /* recycle */, + mStartingPosition); + } + + final BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams( Math.round(mStartingPosition.width()), Math.round(mStartingPosition.height())); initPosition(mStartingPosition, lp); setLayoutParams(lp); } - // TODO(194414938) set correct corner radii - public void update(RectF position, float progress, float windowRadius) { + public void update(RectF bounds, float progress) { MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); - float dX = position.left - mStartingPosition.left; - float dY = position.top - lp.topMargin; + float dX = bounds.left - mStartingPosition.left; + float dY = bounds.top - lp.topMargin; + float scaleX = bounds.width() / lp.width; + float scaleY = bounds.height() / lp.height; + + mFullscreenParams.updateParams(bounds, progress, scaleX, scaleY); setTranslationX(dX); setTranslationY(dY); - - float scaleX = position.width() / lp.width; - float scaleY = position.height() / lp.height; setScaleX(scaleX); setScaleY(scaleY); + mSplitPlaceholderView.invalidate(); + mThumbnailView.invalidate(); + float childScaleX = 1f / scaleX; float childScaleY = 1f / scaleY; - - invalidate(); - // TODO(194414938) seems like this scale value could be fine tuned, some stretchiness - mImageView.setScaleX(1f / scaleX + scaleX * progress); - mImageView.setScaleY(1f / scaleY + scaleY * progress); mOrientationHandler.setPrimaryScale(mSplitPlaceholderView.getIconView(), childScaleX); mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY); } @@ -156,6 +213,10 @@ public class FloatingTaskView extends FrameLayout { mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); } + public void setIcon(Bitmap icon) { + mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize); + } + protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) { mStartingPosition.set(pos); lp.ignoreInsets = true; @@ -173,40 +234,104 @@ public class FloatingTaskView extends FrameLayout { layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); } - public void addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, - View viewToCover, boolean fadeWithThumbnail) { + /** + * Animates a FloatingTaskThumbnailView and its overlapping SplitPlaceholderView when a split + * is staged. + */ + public void addStagingAnimation(PendingAnimation animation, RectF startingBounds, + Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { + boolean isTablet = mActivity.getDeviceProfile().isTablet; + boolean splittingFromOverview = fadeWithThumbnail; + SplitAnimationTimings timings; + + if (isTablet && splittingFromOverview) { + timings = SplitAnimationTimings.TABLET_OVERVIEW_TO_SPLIT; + } else if (!isTablet && splittingFromOverview) { + timings = SplitAnimationTimings.PHONE_OVERVIEW_TO_SPLIT; + } else { + // Splitting from Home is currently only available on tablets + timings = SplitAnimationTimings.TABLET_HOME_TO_SPLIT; + } + + addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask, + timings); + } + + /** + * Animates the FloatingTaskThumbnailView and SplitPlaceholderView for the two thumbnails + * when a split is confirmed. + */ + public void addConfirmAnimation(PendingAnimation animation, RectF startingBounds, + Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { + SplitAnimationTimings timings = + AnimUtils.getDeviceSplitToConfirmTimings(mActivity.getDeviceProfile().isTablet); + + addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask, + timings); + } + + /** + * Sets up and builds a split staging animation. + * Called by {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} and + * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)}. + */ + public void addAnimation(PendingAnimation animation, RectF startingBounds, + Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask, + SplitAnimationTimings timings) { + mFullscreenParams.setIsStagedTask(isStagedTask); final BaseDragLayer dragLayer = mActivity.getDragLayer(); int[] dragLayerBounds = new int[2]; dragLayer.getLocationOnScreen(dragLayerBounds); SplitOverlayProperties prop = new SplitOverlayProperties(endBounds, - startingBounds, viewToCover, dragLayerBounds[0], - dragLayerBounds[1]); + startingBounds, dragLayerBounds[0], dragLayerBounds[1]); ValueAnimator transitionAnimator = ValueAnimator.ofFloat(0, 1); animation.add(transitionAnimator); long animDuration = animation.getDuration(); - Rect crop = new Rect(); RectF floatingTaskViewBounds = new RectF(); - final float initialWindowRadius = supportsRoundedCornersOnWindows(getResources()) - ? Math.max(crop.width(), crop.height()) / 2f - : 0f; if (fadeWithThumbnail) { - animation.addFloat(mSplitPlaceholderView, SplitPlaceholderView.ALPHA_FLOAT, - 0, 1, ACCEL); - animation.addFloat(mImageView, LauncherAnimUtils.VIEW_ALPHA, - 1, 0, DEACCEL_3); + // This code block runs for the placeholder view during Overview > OverviewSplitSelect + // and for the selected (secondary) thumbnail during OverviewSplitSelect > Confirmed + + // FloatingTaskThumbnailView: thumbnail fades out to transparent + animation.setViewAlpha(mThumbnailView, 0, clampToProgress(LINEAR, + timings.getPlaceholderFadeInStartOffset(), + timings.getPlaceholderFadeInEndOffset())); + + // SplitPlaceholderView: gray background fades in at same time, then new icon fades in + fadeInSplitPlaceholder(animation, timings); + } else if (isStagedTask) { + // This code block runs for the placeholder view during Normal > OverviewSplitSelect + // and for the placeholder (primary) thumbnail during OverviewSplitSelect > Confirmed + + // Fade in the placeholder view during Normal > OverviewSplitSelect + if (mSplitPlaceholderView.getAlpha() == 0) { + mSplitPlaceholderView.getIconView().setAlpha(0); + fadeInSplitPlaceholder(animation, timings); + } + + // No-op for placeholder during OverviewSplitSelect > Confirmed, alpha should be set } MultiValueUpdateListener listener = new MultiValueUpdateListener() { - final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, - initialWindowRadius, 0, animDuration, LINEAR); - final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration, LINEAR); - final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration, LINEAR); - final FloatProp mTaskViewScaleX = new FloatProp(prop.initialTaskViewScaleX, - prop.finalTaskViewScaleX, 0, animDuration, LINEAR); - final FloatProp mTaskViewScaleY = new FloatProp(prop.initialTaskViewScaleY, - prop.finalTaskViewScaleY, 0, animDuration, LINEAR); + // SplitPlaceholderView: rectangle translates and stretches to new position + final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration, + clampToProgress(timings.getStagedRectXInterpolator(), + timings.getStagedRectSlideStartOffset(), + timings.getStagedRectSlideEndOffset())); + final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration, + clampToProgress(timings.getStagedRectYInterpolator(), + timings.getStagedRectSlideStartOffset(), + timings.getStagedRectSlideEndOffset())); + final FloatProp mTaskViewScaleX = new FloatProp(1f, prop.finalTaskViewScaleX, 0, + animDuration, clampToProgress(timings.getStagedRectScaleXInterpolator(), + timings.getStagedRectSlideStartOffset(), + timings.getStagedRectSlideEndOffset())); + final FloatProp mTaskViewScaleY = new FloatProp(1f, prop.finalTaskViewScaleY, 0, + animDuration, clampToProgress(timings.getStagedRectScaleYInterpolator(), + timings.getStagedRectSlideStartOffset(), + timings.getStagedRectSlideEndOffset())); @Override public void onUpdate(float percent, boolean initOnly) { // Calculate the icon position. @@ -215,32 +340,68 @@ public class FloatingTaskView extends FrameLayout { Utilities.scaleRectFAboutCenter(floatingTaskViewBounds, mTaskViewScaleX.value, mTaskViewScaleY.value); - update(floatingTaskViewBounds, percent, mWindowRadius.value * 1); + update(floatingTaskViewBounds, percent); } }; + transitionAnimator.addUpdateListener(listener); } + void fadeInSplitPlaceholder(PendingAnimation animation, SplitAnimationTimings timings) { + animation.setViewAlpha(mSplitPlaceholderView, 1, clampToProgress(LINEAR, + timings.getPlaceholderFadeInStartOffset(), + timings.getPlaceholderFadeInEndOffset())); + animation.setViewAlpha(mSplitPlaceholderView.getIconView(), 1, clampToProgress(LINEAR, + timings.getPlaceholderIconFadeInStartOffset(), + timings.getPlaceholderIconFadeInEndOffset())); + } + + void drawRoundedRect(Canvas canvas, Paint paint) { + if (mFullscreenParams == null) { + return; + } + + canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), + mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleX, + mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleY, + paint); + } + + /** + * When a split is staged, center the icon in the staging area. Accounts for device insets. + * @param iconView The icon that should be centered. + * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is + * offscreen). + * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is + * offscreen). + */ + void centerIconView(IconView iconView, float onScreenRectCenterX, float onScreenRectCenterY) { + mOrientationHandler.updateSplitIconParams(iconView, onScreenRectCenterX, + onScreenRectCenterY, mFullscreenParams.mScaleX, mFullscreenParams.mScaleY, + iconView.getDrawableWidth(), iconView.getDrawableHeight(), + mActivity.getDeviceProfile(), mStagePosition); + } + + public int getStagePosition() { + return mStagePosition; + } + private static class SplitOverlayProperties { - private final float initialTaskViewScaleX; - private final float initialTaskViewScaleY; private final float finalTaskViewScaleX; private final float finalTaskViewScaleY; private final float dX; private final float dY; - SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, View view, + SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, int dragLayerLeft, int dragLayerTop) { float maxScaleX = endBounds.width() / startTaskViewBounds.width(); float maxScaleY = endBounds.height() / startTaskViewBounds.height(); - initialTaskViewScaleX = view.getScaleX(); - initialTaskViewScaleY = view.getScaleY(); finalTaskViewScaleX = maxScaleX; finalTaskViewScaleY = maxScaleY; - // Animate the app icon to the center of the window bounds in screen coordinates. + // Animate to the center of the window bounds in screen coordinates. float centerX = endBounds.centerX() - dragLayerLeft; float centerY = endBounds.centerY() - dragLayerTop; @@ -248,4 +409,34 @@ public class FloatingTaskView extends FrameLayout { dY = centerY - startTaskViewBounds.centerY(); } } + + public static class FullscreenDrawParams { + + private final float mCornerRadius; + private final float mWindowCornerRadius; + public boolean mIsStagedTask; + public final RectF mBounds = new RectF(); + public float mCurrentDrawnCornerRadius; + public float mScaleX = 1; + public float mScaleY = 1; + + public FullscreenDrawParams(Context context) { + mCornerRadius = TaskCornerRadius.get(context); + mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context); + + mCurrentDrawnCornerRadius = mCornerRadius; + } + + public void updateParams(RectF bounds, float progress, float scaleX, float scaleY) { + mBounds.set(bounds); + mScaleX = scaleX; + mScaleY = scaleY; + mCurrentDrawnCornerRadius = mIsStagedTask ? mWindowCornerRadius : + Utilities.mapRange(progress, mCornerRadius, mWindowCornerRadius); + } + + public void setIsStagedTask(boolean isStagedTask) { + mIsStagedTask = isStagedTask; + } + } } diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java index 1a73c4b009..e5f241fdbe 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java @@ -27,16 +27,19 @@ import android.view.View; import android.view.ViewOutlineProvider; import android.widget.RemoteViews.RemoteViewOutlineProvider; -import com.android.launcher3.Utilities; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.RoundedCornerEnforcement; import java.util.stream.IntStream; /** - * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a + * Mimics the appearance of the background view of a + * {@link LauncherAppWidgetHostView} through a * an App Widget activity launch animation. */ @TargetApi(Build.VERSION_CODES.S) @@ -66,29 +69,39 @@ final class FloatingWidgetBackgroundView extends View { setClipToOutline(true); } - void init(LauncherAppWidgetHostView hostView, View backgroundView, float finalRadius, - int fallbackBackgroundColor) { + void init(LauncherAppWidgetHostView hostView, @NonNull View backgroundView, + float finalRadius, int fallbackBackgroundColor) { mFinalRadius = finalRadius; mSourceView = backgroundView; mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView); mIsUsingFallback = false; if (isSupportedDrawable(backgroundView.getForeground())) { - mOriginalForeground = backgroundView.getForeground(); + if (backgroundView.getTag(R.id.saved_floating_widget_foreground) == null) { + mOriginalForeground = backgroundView.getForeground(); + backgroundView.setTag(R.id.saved_floating_widget_foreground, mOriginalForeground); + } else { + mOriginalForeground = (Drawable) backgroundView.getTag( + R.id.saved_floating_widget_foreground); + } mForegroundProperties.init( mOriginalForeground.getConstantState().newDrawable().mutate()); setForeground(mForegroundProperties.mDrawable); - Drawable clipPlaceholder = - mOriginalForeground.getConstantState().newDrawable().mutate(); + Drawable clipPlaceholder = mOriginalForeground.getConstantState().newDrawable().mutate(); clipPlaceholder.setAlpha(0); mSourceView.setForeground(clipPlaceholder); } if (isSupportedDrawable(backgroundView.getBackground())) { - mOriginalBackground = backgroundView.getBackground(); + if (backgroundView.getTag(R.id.saved_floating_widget_background) == null) { + mOriginalBackground = backgroundView.getBackground(); + backgroundView.setTag(R.id.saved_floating_widget_background, mOriginalBackground); + } else { + mOriginalBackground = (Drawable) backgroundView.getTag( + R.id.saved_floating_widget_background); + } mBackgroundProperties.init( mOriginalBackground.getConstantState().newDrawable().mutate()); setBackground(mBackgroundProperties.mDrawable); - Drawable clipPlaceholder = - mOriginalBackground.getConstantState().newDrawable().mutate(); + Drawable clipPlaceholder = mOriginalBackground.getConstantState().newDrawable().mutate(); clipPlaceholder.setAlpha(0); mSourceView.setBackground(clipPlaceholder); } else if (mOriginalForeground == null) { @@ -100,7 +113,8 @@ final class FloatingWidgetBackgroundView extends View { /** Update the animated properties of the drawables. */ void update(float cornerRadiusProgress, float fallbackAlpha) { - if (isUninitialized()) return; + if (isUninitialized()) + return; mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius) * cornerRadiusProgress; mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress); @@ -110,12 +124,19 @@ final class FloatingWidgetBackgroundView extends View { /** Restores the drawables to the source view. */ void finish() { - if (isUninitialized()) return; - if (mOriginalForeground != null) mSourceView.setForeground(mOriginalForeground); - if (mOriginalBackground != null) mSourceView.setBackground(mOriginalBackground); + if (isUninitialized()) + return; + if (mOriginalForeground != null) + mSourceView.setForeground(mOriginalForeground); + if (mOriginalBackground != null) + mSourceView.setBackground(mOriginalBackground); } void recycle() { + if (mSourceView != null) { + mSourceView.setTag(R.id.saved_floating_widget_foreground, null); + mSourceView.setTag(R.id.saved_floating_widget_background, null); + } mSourceView = null; mOriginalForeground = null; mOriginalBackground = null; @@ -125,9 +146,12 @@ final class FloatingWidgetBackgroundView extends View { setBackground(null); } - /** Get the largest of drawable corner radii or background view outline radius. */ + /** + * Get the largest of drawable corner radii or background view outline radius. + */ float getMaximumRadius() { - if (isUninitialized()) return 0; + if (isUninitialized()) + return 0; return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground), getMaxRadius(mOriginalBackground))); } @@ -137,12 +161,14 @@ final class FloatingWidgetBackgroundView extends View { } /** Returns the maximum corner radius of {@param drawable}. */ - private static float getMaxRadius(Drawable drawable) { - if (!(drawable instanceof GradientDrawable)) return 0; + private static float getMaxRadius(@Nullable Drawable drawable) { + if (!(drawable instanceof GradientDrawable)) + return 0; float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii(); float cornerRadius = ((GradientDrawable) drawable).getCornerRadius(); - double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length) - .mapToDouble(i -> cornerRadii[i]).max().orElse(0); + double radiiMax = cornerRadii == null ? 0 + : IntStream.range(0, cornerRadii.length) + .mapToDouble(i -> cornerRadii[i]).max().orElse(0); return Math.max(cornerRadius, (float) radiiMax); } @@ -177,7 +203,8 @@ final class FloatingWidgetBackgroundView extends View { /** Store a drawable's animated properties. */ void init(Drawable drawable) { mDrawable = drawable; - if (!(drawable instanceof GradientDrawable)) return; + if (!(drawable instanceof GradientDrawable)) + return; mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius(); mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii(); } @@ -186,11 +213,13 @@ final class FloatingWidgetBackgroundView extends View { * Update the drawable for the given animation state. * * @param finalRadius the radius of each corner when {@param progress} is 1 - * @param progress the linear progress of the corner radius from its original value to + * @param progress the linear progress of the corner radius from its original + * value to * {@param finalRadius} */ void updateDrawable(float finalRadius, float progress) { - if (!(mDrawable instanceof GradientDrawable)) return; + if (!(mDrawable instanceof GradientDrawable)) + return; GradientDrawable d = (GradientDrawable) mDrawable; if (mOriginalRadii != null) { for (int i = 0; i < mOriginalRadii.length; i++) { diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java index 252362a75c..9d668265d3 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java @@ -25,6 +25,7 @@ import android.os.Build; import android.util.AttributeSet; import android.util.Size; import android.view.GhostView; +import android.view.RemoteAnimationTarget; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; @@ -41,7 +42,6 @@ import com.android.launcher3.views.FloatingView; import com.android.launcher3.views.ListenerView; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.RoundedCornerEnforcement; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; /** A view that mimics an App Widget through a launch animation. */ @TargetApi(Build.VERSION_CODES.S) @@ -122,7 +122,8 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, @Override public void onGlobalLayout() { - if (isUninitialized()) return; + if (isUninitialized()) + return; positionViews(); if (mOnTargetChangeRunnable != null) { mOnTargetChangeRunnable.run(); @@ -142,7 +143,8 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, /** Callback at the end or early exit of the animation. */ @Override public void fastFinish() { - if (isUninitialized()) return; + if (isUninitialized()) + return; Runnable fastFinishRunnable = mFastFinishRunnable; if (fastFinishRunnable != null) { fastFinishRunnable.run(); @@ -187,18 +189,23 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, /** * Updates the position and opacity of the floating widget's components. * - * @param backgroundPosition the new position of the widget's background relative to the + * @param backgroundPosition the new position of the widget's background + * relative to the * {@link FloatingWidgetView}'s parent - * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView} + * @param floatingWidgetAlpha the overall opacity of the + * {@link FloatingWidgetView} * @param foregroundAlpha the opacity of the foreground layer - * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App + * @param fallbackBackgroundAlpha the opacity of the fallback background used + * when the App * Widget doesn't have a background - * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the + * @param cornerRadiusProgress progress of the corner radius animation, where + * 0 is the * original radius and 1 is the window radius */ public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha, float fallbackBackgroundAlpha, float cornerRadiusProgress) { - if (isUninitialized() || mAppTargetIsTranslucent) return; + if (isUninitialized() || mAppTargetIsTranslucent) + return; setAlpha(floatingWidgetAlpha); mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha); mAppWidgetView.setAlpha(foregroundAlpha); @@ -212,7 +219,10 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, onGlobalLayout(); } - /** Sets the layout parameters of the floating view and its background view child. */ + /** + * Sets the layout parameters of the floating view and its background view + * child. + */ private void positionViews() { LayoutParams layoutParams = (LayoutParams) getLayoutParams(); layoutParams.setMargins(0, 0, 0, 0); @@ -230,8 +240,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, if (mForegroundOverlayView != null) { sTmpMatrix.reset(); - float foregroundScale = - mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth(); + float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth(); sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(), -mBackgroundOffset.top - mAppWidgetView.getTop()); sTmpMatrix.postScale(foregroundScale, foregroundScale); @@ -275,10 +284,12 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, } /** - * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of + * Configures and returns a an instance of {@link FloatingWidgetView} matching + * the appearance of * {@param originalView}. * - * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's + * @param widgetBackgroundPosition a {@link RectF} that will be updated with the + * widget's * background bounds * @param windowSize the size of the window when launched * @param windowCornerRadius the corner radius of the window @@ -289,8 +300,8 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, int fallbackBackgroundColor) { final DragLayer dragLayer = launcher.getDragLayer(); ViewGroup parent = (ViewGroup) dragLayer.getParent(); - FloatingWidgetView floatingView = - launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent); + FloatingWidgetView floatingView = launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, + parent); floatingView.recycle(); floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize, @@ -300,18 +311,19 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, } /** - * Extract a background color from a target's task description, or fall back to the given + * Extract a background color from a target's task description, or fall back to + * the given * context's theme background color. */ public static int getDefaultBackgroundColor( - Context context, RemoteAnimationTargetCompat target) { - return (target != null && target.taskInfo != null && target.taskInfo.taskDescription != null) + Context context, RemoteAnimationTarget target) { + return (target != null && target.taskInfo.taskDescription != null) ? target.taskInfo.taskDescription.getBackgroundColor() : Themes.getColorBackground(context); } private static void getRelativePosition(View descendant, View ancestor, RectF position) { - float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()}; + float[] points = new float[] { 0, 0, descendant.getWidth(), descendant.getHeight() }; Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points, false /* includeRootScroll */, true /* ignoreTransform */); position.set( diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index 7e4f9d0abc..5bfd0350b5 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -1,28 +1,38 @@ package com.android.quickstep.views; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; 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 android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.util.RunnableList; -import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; +import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.launcher3.util.TransformingTouchDelegate; import com.android.quickstep.RecentsModel; import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SplitSelectStateController; +import com.android.quickstep.util.TaskViewSimulator; 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.android.systemui.shared.system.InteractionJankMonitorWrapper; import java.util.HashMap; import java.util.function.Consumer; @@ -49,10 +59,9 @@ public class GroupedTaskView extends TaskView { private CancellableTask mIconLoadRequest2; private final float[] mIcon2CenterCoords = new float[2]; private TransformingTouchDelegate mIcon2TouchDelegate; - @Nullable private StagedSplitBounds mSplitBoundsConfig; + @Nullable private SplitBounds mSplitBoundsConfig; private final DigitalWellBeingToast mDigitalWellBeingToast2; - public GroupedTaskView(Context context) { this(context, null); } @@ -66,6 +75,23 @@ public class GroupedTaskView extends TaskView { mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this); } + @Override + protected void updateBorderBounds(Rect bounds) { + if (mSplitBoundsConfig == null) { + super.updateBorderBounds(bounds); + return; + } + bounds.set( + Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()), + mSnapshotView2.getLeft() + Math.round(mSnapshotView2.getTranslationX())), + Math.min(mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()), + mSnapshotView2.getTop() + Math.round(mSnapshotView2.getTranslationY())), + Math.max(mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()), + mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())), + Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()), + mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY()))); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -75,15 +101,41 @@ public class GroupedTaskView extends TaskView { } public void bind(Task primary, Task secondary, RecentsOrientedState orientedState, - @Nullable StagedSplitBounds splitBoundsConfig) { + @Nullable SplitBounds splitBoundsConfig) { super.bind(primary, orientedState); mSecondaryTask = secondary; mTaskIdContainer[1] = secondary.key.id; mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2, mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT); - mTaskIdAttributeContainer[0].setStagePosition(STAGE_POSITION_TOP_OR_LEFT); + mTaskIdAttributeContainer[0].setStagePosition( + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT); mSnapshotView2.bind(secondary); mSplitBoundsConfig = splitBoundsConfig; + if (mSplitBoundsConfig == null) { + return; + } + mSnapshotView.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator + .convertSplitBounds(splitBoundsConfig), + PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT); + mSnapshotView2.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator + .convertSplitBounds(splitBoundsConfig), + PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT); + } + + /** + * Sets up an on-click listener and the visibility for show_windows icon on top of each task. + */ + @Override + public void setUpShowAllInstancesListener() { + // sets up the listener for the left/top task + super.setUpShowAllInstancesListener(); + + // right/bottom task's base package name + String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName(); + + // icon of the right/bottom task + View showWindowsView = findViewById(R.id.show_windows_right); + updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)); } @Override @@ -123,8 +175,8 @@ public class GroupedTaskView extends TaskView { } } - public void updateSplitBoundsConfig(StagedSplitBounds stagedSplitBounds) { - mSplitBoundsConfig = stagedSplitBounds; + public void updateSplitBoundsConfig(SplitBounds splitBounds) { + mSplitBoundsConfig = splitBounds; invalidate(); } @@ -169,8 +221,14 @@ public class GroupedTaskView extends TaskView { RunnableList endCallback = new RunnableList(); RecentsView recentsView = getRecentsView(); // Callbacks run from remote animation when recents animation not currently running - recentsView.getSplitPlaceholder().launchTasks(this /*groupedTaskView*/, - success -> endCallback.executeAllAndDestroy(), + InteractionJankMonitorWrapper.begin(this, + InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Enter form GroupedTaskView"); + recentsView.getSplitSelectController().launchTasks(this /*groupedTaskView*/, + success -> { + endCallback.executeAllAndDestroy(); + InteractionJankMonitorWrapper.end( + InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); + }, false /* freezeTaskList */); // Callbacks get run from recentsView for case when recents animation already running @@ -180,8 +238,8 @@ public class GroupedTaskView extends TaskView { @Override public void launchTask(@NonNull Consumer callback, boolean freezeTaskList) { - getRecentsView().getSplitPlaceholder().launchTasks(mTask, mSecondaryTask, - STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList, + getRecentsView().getSplitSelectController().launchTasks(mTask.key.id, mSecondaryTask.key.id, + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList, getSplitRatio()); } @@ -199,11 +257,45 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.refresh(); } + @Override + public boolean containsTaskId(int taskId) { + return (mTask != null && mTask.key.id == taskId) + || (mSecondaryTask != null && mSecondaryTask.key.id == taskId); + } + @Override public TaskThumbnailView[] getThumbnails() { return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2}; } + @Override + protected int getLastSelectedChildTaskIndex() { + SplitSelectStateController splitSelectController = + getRecentsView().getSplitSelectController(); + if (splitSelectController.isDismissingFromSplitPair()) { + // return the container index of the task that wasn't initially selected to split with + // because that is the only remaining app that can be selected. The coordinate checks + // below aren't reliable since both of those views may be gone/transformed + int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); + if (initSplitTaskId != INVALID_TASK_ID) { + return initSplitTaskId == mTask.key.id ? 1 : 0; + } + } + + // Check which of the two apps was selected + if (isCoordInView(mIconView2, mLastTouchDownPosition) + || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { + return 1; + } + return super.getLastSelectedChildTaskIndex(); + } + + private boolean isCoordInView(View v, PointF position) { + float[] localPos = new float[]{position.x, position.y}; + Utilities.mapCoordInSelfToDescendant(v, this, localPos); + return Utilities.pointInView(v, localPos[0], localPos[1], 0f /* slop */); + } + @Override public void onRecycle() { super.onRecycle(); @@ -220,28 +312,49 @@ public class GroupedTaskView extends TaskView { if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { return; } - getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, - mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, - mActivity.getDeviceProfile()); + int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); + if (initSplitTaskId == INVALID_TASK_ID) { + getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, + mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, + mActivity.getDeviceProfile(), getLayoutDirection() == 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 + mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); + mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); + mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); + mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); + } else { + // Currently being split with this taskView, let the non-split selected thumbnail + // take up full thumbnail area + TaskIdAttributeContainer container = + mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; + container.getThumbnailView().measure(widthMeasureSpec, + View.MeasureSpec.makeMeasureSpec( + heightSize - + mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, + MeasureSpec.EXACTLY)); + } updateIconPlacement(); } @Override public void setOverlayEnabled(boolean overlayEnabled) { - super.setOverlayEnabled(overlayEnabled); - mSnapshotView2.setOverlayEnabled(overlayEnabled); + // Intentional no-op to prevent setting smart actions overlay on thumbnails } @Override public void setOrientationState(RecentsOrientedState orientationState) { super.setOrientationState(orientationState); DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - boolean isGridTask = deviceProfile.overviewShowAsGrid && !isFocusedTask(); + boolean isGridTask = deviceProfile.isTablet && !isFocusedTask(); int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx : deviceProfile.overviewTaskIconDrawableSizePx; mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize); mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated()); updateIconPlacement(); + updateSecondaryDwbPlacement(); } private void updateIconPlacement() { @@ -255,7 +368,15 @@ public class GroupedTaskView extends TaskView { getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2, taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(), - isRtl, deviceProfile, mSplitBoundsConfig); + getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile, + mSplitBoundsConfig); + } + + private void updateSecondaryDwbPlacement() { + if (mSecondaryTask == null) { + return; + } + mDigitalWellBeingToast2.initialize(mSecondaryTask); } @Override @@ -265,14 +386,12 @@ public class GroupedTaskView extends TaskView { } @Override - protected void setIconAndDimTransitionProgress(float progress, boolean invert) { - super.setIconAndDimTransitionProgress(progress, invert); + protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { + super.setIconsAndBannersTransitionProgress(progress, invert); // Value set by super call float scale = mIconView.getAlpha(); mIconView2.setAlpha(scale); - mDigitalWellBeingToast2.updateBannerOffset(1f - scale, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mDigitalWellBeingToast2.updateBannerOffset(1f - scale); } @Override @@ -282,4 +401,45 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.setDimAlpha(amount); mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount); } + + @Override + protected void applyThumbnailSplashAlpha() { + super.applyThumbnailSplashAlpha(); + mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha); + } + + @Override + protected void refreshTaskThumbnailSplash() { + super.refreshTaskThumbnailSplash(); + mSnapshotView2.refreshSplashView(); + } + + @Override + protected void resetViewTransforms() { + super.resetViewTransforms(); + mSnapshotView2.resetViewTransforms(); + } + + /** + * Sets visibility for thumbnails and associated elements (DWB banners). + * IconView is unaffected. + * + * When setting INVISIBLE, sets the visibility for the last selected child task. + * When setting VISIBLE (as a reset), sets the visibility for both tasks. + */ + @Override + void setThumbnailVisibility(int visibility, int taskId) { + if (visibility == VISIBLE) { + mSnapshotView.setVisibility(visibility); + mDigitalWellBeingToast.setBannerVisibility(visibility); + mSnapshotView2.setVisibility(visibility); + mDigitalWellBeingToast2.setBannerVisibility(visibility); + } else if (taskId == getTaskIds()[0]) { + mSnapshotView.setVisibility(visibility); + mDigitalWellBeingToast.setBannerVisibility(visibility); + } else { + mSnapshotView2.setVisibility(visibility); + mDigitalWellBeingToast2.setBannerVisibility(visibility); + } + } } diff --git a/quickstep/src/com/android/quickstep/views/LaunchableConstraintLayout.kt b/quickstep/src/com/android/quickstep/views/LaunchableConstraintLayout.kt new file mode 100644 index 0000000000..537eca132a --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/LaunchableConstraintLayout.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** A [ConstraintLayout] that also implements [LaunchableView]. */ +open class LaunchableConstraintLayout : ConstraintLayout, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + // Note that super.setVisibility() is passed to the delegate upon creation and called by it. + // This method is just a passthrough if no animation is in progress, whereas otherwise it + // caches the passed value and restores it at the end of the animation. + delegate.setVisibility(visibility) + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index 7e1d29ee84..c165accc52 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -15,38 +15,46 @@ */ package com.android.quickstep.views; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; import static com.android.launcher3.LauncherState.SPRING_LOADED; -import static com.android.launcher3.testing.TestProtocol.BAD_STATE; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.view.MotionEvent; import android.view.Surface; import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; -import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.LauncherState; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.statehandlers.DepthController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.StateManager.StateListener; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; +import com.android.quickstep.GestureState; import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.RotationTouchHelper; +import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.SplitSelectStateController; +import com.android.systemui.shared.recents.model.Task; /** * {@link RecentsView} used in Launcher activity */ @TargetApi(Build.VERSION_CODES.O) -public class LauncherRecentsView extends RecentsView +public class LauncherRecentsView extends RecentsView implements StateListener { public LauncherRecentsView(Context context) { @@ -66,7 +74,6 @@ public class LauncherRecentsView extends RecentsView

We want to show the splash if the aspect ratio or rotation of the thumbnail would be + * different from the task. + */ + public boolean shouldShowSplashView() { + return isThumbnailAspectRatioDifferentFromThumbnailData() + || isThumbnailRotationDifferentFromTask() + || mShowSplashForSplitSelection; + } + + public void setShowSplashForSplitSelection(boolean showSplashForSplitSelection) { + mShowSplashForSplitSelection = showSplashForSplitSelection; + } + + protected void refreshSplashView() { + if (mTask != null) { + updateSplashView(mTask.icon); + invalidate(); + } + } + + private void updateSplashView(Drawable icon) { + if (icon == null || icon.getConstantState() == null) { + mSplashViewDrawable = null; + mSplashView = null; + return; + } + mSplashViewDrawable = icon.getConstantState().newDrawable().mutate(); + mSplashViewDrawable.setAlpha(mSplashAlpha); + ImageView imageView = mSplashView == null ? new ImageView(getContext()) : mSplashView; + imageView.setImageDrawable(mSplashViewDrawable); + + imageView.setScaleType(ImageView.ScaleType.MATRIX); + Matrix matrix = new Matrix(); + float drawableWidth = mSplashViewDrawable.getIntrinsicWidth(); + float drawableHeight = mSplashViewDrawable.getIntrinsicHeight(); + float viewWidth = getMeasuredWidth(); + float viewCenterX = viewWidth / 2f; + float viewHeight = getMeasuredHeight(); + float viewCenterY = viewHeight / 2f; + float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f; + float centeredDrawableTop = (viewHeight - drawableHeight) / 2f; + float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale(); + float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null + ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen(); + float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX()); + float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY()); + + // Center the image in the view. + matrix.setTranslate(centeredDrawableLeft, centeredDrawableTop); + // Apply scale transformation after translation, pivoting around center of view. + matrix.postScale(scaleX, scaleY, viewCenterX, viewCenterY); + + imageView.setImageMatrix(matrix); + mSplashView = imageView; + } + + private boolean isThumbnailAspectRatioDifferentFromThumbnailData() { + if (mThumbnailData == null || mThumbnailData.thumbnail == null) { + return false; + } + + float thumbnailViewAspect = getWidth() / (float) getHeight(); + float thumbnailDataAspect = + mThumbnailData.thumbnail.getWidth() / (float) mThumbnailData.thumbnail.getHeight(); + + return isRelativePercentDifferenceGreaterThan(thumbnailViewAspect, + thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); + } + + private boolean isThumbnailRotationDifferentFromTask() { + RecentsView recents = getTaskView().getRecentsView(); + if (recents == null || mThumbnailData == null) { + return false; + } + + if (recents.getPagedOrientationHandler() == PagedOrientationHandler.PORTRAIT) { + int currentRotation = recents.getPagedViewOrientedState().getRecentsActivityRotation(); + return (currentRotation - mThumbnailData.rotation) % 2 != 0; + } else { + return recents.getPagedOrientationHandler().getRotation() != mThumbnailData.rotation; + } + } + /** * Potentially re-init the task overlay. Be cautious when calling this as the overlay may * do processing on initialization. */ private void refreshOverlay() { if (mOverlayEnabled) { - getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix, - mPreviewPositionHelper.mIsOrientationChanged); + getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(), + mPreviewPositionHelper.isOrientationChanged()); } else { getTaskOverlay().reset(); } @@ -361,7 +549,8 @@ public class TaskThumbnailView extends View { } private void updateThumbnailMatrix() { - mPreviewPositionHelper.mIsOrientationChanged = false; + DeviceProfile dp = mActivity.getDeviceProfile(); + mPreviewPositionHelper.setOrientationChanged(false); if (mBitmapShader != null && mThumbnailData != null) { mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight()); @@ -369,10 +558,10 @@ public class TaskThumbnailView extends View { .getRecentsActivityRotation(); boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, - getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(), - currentRotation, isRtl); + getMeasuredWidth(), getMeasuredHeight(), dp.widthPx, dp.heightPx, + dp.taskbarHeight, dp.isTablet, currentRotation, isRtl); - mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix); + mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix()); mPaint.setShader(mBitmapShader); } getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper); @@ -412,257 +601,4 @@ public class TaskThumbnailView extends View { } return mThumbnailData.isRealSnapshot && !mTask.isLocked; } - - /** - * Utility class to position the thumbnail in the TaskView - */ - public static class PreviewPositionHelper { - - private static final RectF EMPTY_RECT_F = new RectF(); - - // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. - private final RectF mClippedInsets = new RectF(); - private final Matrix mMatrix = new Matrix(); - private boolean mIsOrientationChanged; - - public Matrix getMatrix() { - return mMatrix; - } - - /** - * Updates the matrix based on the provided parameters - */ - public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, - int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation, - boolean isRtl) { - boolean isRotated = false; - boolean isOrientationDifferent; - - int thumbnailRotation = thumbnailData.rotation; - int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); - RectF thumbnailClipHint = new RectF(); - if (TaskView.clipLeft(dp)) { - thumbnailClipHint.left = thumbnailData.insets.left; - } - if (TaskView.clipRight(dp)) { - thumbnailClipHint.right = thumbnailData.insets.right; - } - if (TaskView.clipTop(dp)) { - thumbnailClipHint.top = thumbnailData.insets.top; - } - if (TaskView.clipBottom(dp)) { - thumbnailClipHint.bottom = thumbnailData.insets.bottom; - } - - float scale = thumbnailData.scale; - final float thumbnailScale; - - // Landscape vs portrait change. - // Note: Disable rotation in grid layout. - boolean windowingModeSupportsRotation = !dp.isMultiWindowMode - && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN - && !dp.overviewShowAsGrid; - isOrientationDifferent = isOrientationChange(deltaRotate) - && windowingModeSupportsRotation; - if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) { - // If we haven't measured , skip the thumbnail drawing and only draw the background - // color - thumbnailScale = 0f; - } else { - // Rotate the screenshot if not in multi-window mode - isRotated = deltaRotate > 0 && windowingModeSupportsRotation; - - float surfaceWidth = thumbnailBounds.width() / scale; - float surfaceHeight = thumbnailBounds.height() / scale; - float availableWidth = surfaceWidth - - (thumbnailClipHint.left + thumbnailClipHint.right); - float availableHeight = surfaceHeight - - (thumbnailClipHint.top + thumbnailClipHint.bottom); - - float canvasAspect = canvasWidth / (float) canvasHeight; - float availableAspect = isRotated - ? availableHeight / availableWidth - : availableWidth / availableHeight; - boolean isAspectLargelyDifferent = Utilities.isRelativePercentDifferenceGreaterThan( - canvasAspect, availableAspect, 0.1f); - if (isRotated && isAspectLargelyDifferent) { - // Do not rotate thumbnail if it would not improve fit - isRotated = false; - isOrientationDifferent = false; - } - - if (isAspectLargelyDifferent) { - // Crop letterbox insets if insets isn't already clipped - if (!TaskView.clipLeft(dp)) { - thumbnailClipHint.left = thumbnailData.letterboxInsets.left; - } - if (!TaskView.clipRight(dp)) { - thumbnailClipHint.right = thumbnailData.letterboxInsets.right; - } - if (!TaskView.clipTop(dp)) { - thumbnailClipHint.top = thumbnailData.letterboxInsets.top; - } - if (!TaskView.clipBottom(dp)) { - thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom; - } - availableWidth = surfaceWidth - - (thumbnailClipHint.left + thumbnailClipHint.right); - availableHeight = surfaceHeight - - (thumbnailClipHint.top + thumbnailClipHint.bottom); - } - - final float targetW, targetH; - if (isOrientationDifferent) { - targetW = canvasHeight; - targetH = canvasWidth; - } else { - targetW = canvasWidth; - targetH = canvasHeight; - } - float targetAspect = targetW / targetH; - - // Update the clipHint such that - // > the final clipped position has same aspect ratio as requested by canvas - // > first fit the width and crop the extra height - // > if that will leave empty space, fit the height and crop the width instead - float croppedWidth = availableWidth; - float croppedHeight = croppedWidth / targetAspect; - if (croppedHeight > availableHeight) { - croppedHeight = availableHeight; - if (croppedHeight < targetH) { - croppedHeight = Math.min(targetH, surfaceHeight); - } - croppedWidth = croppedHeight * targetAspect; - - // One last check in case the task aspect radio messed up something - if (croppedWidth > surfaceWidth) { - croppedWidth = surfaceWidth; - croppedHeight = croppedWidth / targetAspect; - } - } - - // Update the clip hints. Align to 0,0, crop the remaining. - if (isRtl) { - thumbnailClipHint.left += availableWidth - croppedWidth; - if (thumbnailClipHint.right < 0) { - thumbnailClipHint.left += thumbnailClipHint.right; - thumbnailClipHint.right = 0; - } - } else { - thumbnailClipHint.right += availableWidth - croppedWidth; - if (thumbnailClipHint.left < 0) { - thumbnailClipHint.right += thumbnailClipHint.left; - thumbnailClipHint.left = 0; - } - } - thumbnailClipHint.bottom += availableHeight - croppedHeight; - if (thumbnailClipHint.top < 0) { - thumbnailClipHint.bottom += thumbnailClipHint.top; - thumbnailClipHint.top = 0; - } else if (thumbnailClipHint.bottom < 0) { - thumbnailClipHint.top += thumbnailClipHint.bottom; - thumbnailClipHint.bottom = 0; - } - - thumbnailScale = targetW / (croppedWidth * scale); - } - - Rect splitScreenInsets = dp.getInsets(); - if (!isRotated) { - // No Rotation - if (dp.isMultiWindowMode) { - mClippedInsets.offsetTo(splitScreenInsets.left * scale, - splitScreenInsets.top * scale); - } else { - mClippedInsets.offsetTo(thumbnailClipHint.left * scale, - thumbnailClipHint.top * scale); - } - mMatrix.setTranslate( - -thumbnailClipHint.left * scale, - -thumbnailClipHint.top * scale); - } else { - setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds, dp); - } - - final float widthWithInsets; - final float heightWithInsets; - if (isOrientationDifferent) { - widthWithInsets = thumbnailBounds.height() * thumbnailScale; - heightWithInsets = thumbnailBounds.width() * thumbnailScale; - } else { - widthWithInsets = thumbnailBounds.width() * thumbnailScale; - heightWithInsets = thumbnailBounds.height() * thumbnailScale; - } - mClippedInsets.left *= thumbnailScale; - mClippedInsets.top *= thumbnailScale; - - if (dp.isMultiWindowMode) { - mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale; - mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale; - } else { - mClippedInsets.right = Math.max(0, - widthWithInsets - mClippedInsets.left - canvasWidth); - mClippedInsets.bottom = Math.max(0, - heightWithInsets - mClippedInsets.top - canvasHeight); - } - - mMatrix.postScale(thumbnailScale, thumbnailScale); - mIsOrientationChanged = isOrientationDifferent; - } - - private int getRotationDelta(int oldRotation, int newRotation) { - int delta = newRotation - oldRotation; - if (delta < 0) delta += 4; - return delta; - } - - /** - * @param deltaRotation the number of 90 degree turns from the current orientation - * @return {@code true} if the change in rotation results in a shift from landscape to - * portrait or vice versa, {@code false} otherwise - */ - private boolean isOrientationChange(int deltaRotation) { - return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; - } - - private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale, - Rect thumbnailPosition, DeviceProfile dp) { - float newLeftInset = 0; - float newTopInset = 0; - float translateX = 0; - float translateY = 0; - - mMatrix.setRotate(90 * deltaRotate); - switch (deltaRotate) { /* Counter-clockwise */ - case Surface.ROTATION_90: - newLeftInset = thumbnailInsets.bottom; - newTopInset = thumbnailInsets.left; - translateX = thumbnailPosition.height(); - break; - case Surface.ROTATION_270: - newLeftInset = thumbnailInsets.top; - newTopInset = thumbnailInsets.right; - translateY = thumbnailPosition.width(); - break; - case Surface.ROTATION_180: - newLeftInset = -thumbnailInsets.top; - newTopInset = -thumbnailInsets.left; - translateX = thumbnailPosition.width(); - translateY = thumbnailPosition.height(); - break; - } - mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale); - mMatrix.postTranslate(translateX, translateY); - if (TaskView.useFullThumbnail(dp)) { - mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top); - } - } - - /** - * Insets to used for clipping the thumbnail (in case it is drawing outside its own space) - */ - public RectF getInsetsToDrawInFullscreen(DeviceProfile dp) { - return TaskView.useFullThumbnail(dp) ? mClippedInsets : EMPTY_RECT_F; - } - } } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index d046fefda8..42589ced5d 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -16,22 +16,23 @@ package com.android.quickstep.views; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.Display.DEFAULT_DISPLAY; import static android.widget.Toast.LENGTH_SHORT; -import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; -import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; -import static com.android.launcher3.Utilities.comp; +import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP; 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.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; +import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; +import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -41,21 +42,25 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.IdRes; import android.app.ActivityOptions; +import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; -import android.graphics.Outline; +import android.graphics.Canvas; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Handler; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; +import android.view.Display; import android.view.MotionEvent; +import android.view.RemoteAnimationTarget; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; -import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -65,17 +70,17 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.testing.TestLogging; -import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.ComponentKey; @@ -87,23 +92,22 @@ import com.android.launcher3.util.ViewPool.Reusable; import com.android.quickstep.RecentsModel; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; -import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.TaskUtils; import com.android.quickstep.TaskViewUtils; +import com.android.quickstep.util.BorderAnimator; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.TaskCornerRadius; import com.android.quickstep.util.TransformParams; -import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper; 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.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.ActivityOptionsCompat; import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import java.lang.annotation.Retention; import java.util.Arrays; @@ -121,6 +125,8 @@ public class TaskView extends FrameLayout implements Reusable { private static final String TAG = TaskView.class.getSimpleName(); private static final boolean DEBUG = false; + private static final RectF EMPTY_RECT_F = new RectF(); + public static final int FLAG_UPDATE_ICON = 1; public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1; @@ -134,44 +140,20 @@ public class TaskView extends FrameLayout implements Reusable { @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL}) public @interface TaskDataChanges {} + /** + * Type of task view + */ + @Retention(SOURCE) + @IntDef({Type.SINGLE, Type.GROUPED, Type.DESKTOP}) + public @interface Type { + int SINGLE = 1; + int GROUPED = 2; + int DESKTOP = 3; + } + /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; - /** - * Should the TaskView display clip off the left inset in RecentsView. - */ - public static boolean clipLeft(DeviceProfile deviceProfile) { - return false; - } - - /** - * Should the TaskView display clip off the top inset in RecentsView. - */ - public static boolean clipTop(DeviceProfile deviceProfile) { - return false; - } - - /** - * Should the TaskView display clip off the right inset in RecentsView. - */ - public static boolean clipRight(DeviceProfile deviceProfile) { - return false; - } - - /** - * Should the TaskView display clip off the bottom inset in RecentsView. - */ - public static boolean clipBottom(DeviceProfile deviceProfile) { - return deviceProfile.isTablet; - } - - /** - * Should the TaskView scale down to fit whole thumbnail in fullscreen. - */ - public static boolean useFullThumbnail(DeviceProfile deviceProfile) { - return deviceProfile.isTablet && !deviceProfile.isTaskbarPresentInApps; - } - private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f; private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f; @@ -195,7 +177,7 @@ public class TaskView extends FrameLayout implements Reusable { new FloatProperty("focusTransition") { @Override public void setValue(TaskView taskView, float v) { - taskView.setIconAndDimTransitionProgress(v, false /* invert */); + taskView.setIconsAndBannersTransitionProgress(v, false /* invert */); } @Override @@ -360,15 +342,14 @@ public class TaskView extends FrameLayout implements Reusable { } }; - private final TaskOutlineProvider mOutlineProvider; - @Nullable protected Task mTask; protected TaskThumbnailView mSnapshotView; protected IconView mIconView; protected final DigitalWellBeingToast mDigitalWellBeingToast; - private float mFullscreenProgress; + protected float mFullscreenProgress; private float mGridProgress; + protected float mTaskThumbnailSplashAlpha; private float mNonGridScale = 1; private float mDismissScale = 1; protected final FullscreenDrawParams mCurrentFullscreenParams; @@ -395,7 +376,6 @@ public class TaskView extends FrameLayout implements Reusable { // Used when in SplitScreenSelectState private float mSplitSelectTranslationY; private float mSplitSelectTranslationX; - private float mSplitSelectScrollOffsetPrimary; @Nullable private ObjectAnimator mIconAndDimAnimator; @@ -408,8 +388,8 @@ public class TaskView extends FrameLayout implements Reusable { /** * Index 0 will contain taskID of left/top task, index 1 will contain taskId of bottom/right */ - protected final int[] mTaskIdContainer = new int[]{-1, -1}; - protected final TaskIdAttributeContainer[] mTaskIdAttributeContainer = + protected int[] mTaskIdContainer = new int[]{-1, -1}; + protected TaskIdAttributeContainer[] mTaskIdAttributeContainer = new TaskIdAttributeContainer[2]; private boolean mShowScreenshot; @@ -424,8 +404,12 @@ public class TaskView extends FrameLayout implements Reusable { private final float[] mIconCenterCoords = new float[2]; + protected final PointF mLastTouchDownPosition = new PointF(); + private boolean mIsClickableAsLiveTile = true; + @Nullable private final BorderAnimator mBorderAnimator; + public TaskView(Context context) { this(context, null); } @@ -435,16 +419,49 @@ public class TaskView extends FrameLayout implements Reusable { } public TaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); + } + + public TaskView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mActivity = StatefulActivity.fromContext(context); setOnClickListener(this::onClick); mCurrentFullscreenParams = new FullscreenDrawParams(context); mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this); - mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams, - mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx); - setOutlineProvider(mOutlineProvider); + boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() + || DesktopTaskView.DESKTOP_MODE_SUPPORTED; + + setWillNotDraw(!keyboardFocusHighlightEnabled); + + mBorderAnimator = !keyboardFocusHighlightEnabled + ? null + : new BorderAnimator( + /* borderBoundsBuilder= */ this::updateBorderBounds, + /* borderWidthPx= */ context.getResources().getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width), + /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius, + /* borderColor= */ attrs == null + ? DEFAULT_BORDER_COLOR + : context.getTheme() + .obtainStyledAttributes( + attrs, + R.styleable.TaskView, + defStyleAttr, + defStyleRes) + .getColor( + R.styleable.TaskView_borderColor, + DEFAULT_BORDER_COLOR), + /* invalidateViewCallback= */ TaskView.this::invalidate); + } + + protected void updateBorderBounds(Rect bounds) { + bounds.set(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()), + mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()), + mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()), + mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY())); } public void setTaskViewId(int id) { @@ -488,6 +505,22 @@ public class TaskView extends FrameLayout implements Reusable { mIconTouchDelegate = new TransformingTouchDelegate(mIconView); } + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (mBorderAnimator != null) { + mBorderAnimator.buildAnimator(gainFocus).start(); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mBorderAnimator != null) { + mBorderAnimator.drawBorder(canvas); + } + } + /** * Whether the taskview should take the touch event from parent. Events passed to children * that might require special handling. @@ -526,10 +559,8 @@ public class TaskView extends FrameLayout implements Reusable { return; } mModalness = modalness; - mIconView.setAlpha(comp(modalness)); - mDigitalWellBeingToast.updateBannerOffset(modalness, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mIconView.setAlpha(1 - modalness); + mDigitalWellBeingToast.updateBannerOffset(modalness); } public DigitalWellBeingToast getDigitalWellBeingToast() { @@ -552,6 +583,52 @@ public class TaskView extends FrameLayout implements Reusable { setOrientationState(orientedState); } + /** + * Sets up an on-click listener and the visibility for show_windows icon on top of the task. + */ + public void setUpShowAllInstancesListener() { + String taskPackageName = mTaskIdAttributeContainer[0].mTask.key.getPackageName(); + + // icon of the top/left task + View showWindowsView = findViewById(R.id.show_windows); + updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)); + } + + /** + * Returns a callback that updates the state of the filter and the recents overview + * + * @param taskPackageName package name of the task to filter by + */ + @Nullable + protected View.OnClickListener getFilterUpdateCallback(String taskPackageName) { + View.OnClickListener cb = (view) -> { + // update and apply a new filter + getRecentsView().setAndApplyFilter(taskPackageName); + }; + + if (!getRecentsView().getFilterState().shouldShowFilterUI(taskPackageName)) { + cb = null; + } + return cb; + } + + /** + * Sets the correct visibility and callback on the provided filterView based on whether + * the callback is null or not + */ + protected void updateFilterCallback(@NonNull View filterView, + @Nullable View.OnClickListener callback) { + // Filtering changes alpha instead of the visibility since visibility + // can be altered separately through RecentsView#resetFromSplitSelectionState() + if (callback == null) { + filterView.setAlpha(0); + } else { + filterView.setAlpha(1); + } + + filterView.setOnClickListener(callback); + } + public TaskIdAttributeContainer[] getTaskIdAttributeContainers() { return mTaskIdAttributeContainer; } @@ -561,6 +638,13 @@ public class TaskView extends FrameLayout implements Reusable { return mTask; } + /** + * Check if given {@code taskId} is tracked in this view + */ + public boolean containsTaskId(int taskId) { + return mTask != null && mTask.key.id == taskId; + } + /** * @return integer array of two elements to be size consistent with max number of tasks possible * index 0 will contain the taskId, index 1 will be -1 indicating a null taskID value @@ -573,6 +657,21 @@ public class TaskView extends FrameLayout implements Reusable { return mTaskIdContainer[1] != -1; } + /** + * Returns the TaskIdAttributeContainer corresponding to a given taskId, or null if the TaskView + * does not contain a Task with that ID. + */ + @Nullable + public TaskIdAttributeContainer getTaskAttributesById(int taskId) { + for (TaskIdAttributeContainer attributes : mTaskIdAttributeContainer) { + if (attributes.getTask().key.id == taskId) { + return attributes; + } + } + + return null; + } + public TaskThumbnailView getThumbnail() { return mSnapshotView; } @@ -598,6 +697,48 @@ public class TaskView extends FrameLayout implements Reusable { return mIconView; } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + RecentsView recentsView = getRecentsView(); + if (recentsView == null || getTask() == null) { + return false; + } + SplitSelectStateController splitSelectStateController = + recentsView.getSplitSelectController(); + // Disable taps for split selection animation unless we have multiple tasks + boolean disableTapsForSplitSelect = + splitSelectStateController.isSplitSelectActive() + && splitSelectStateController.getInitialTaskId() == getTask().key.id + && !containsMultipleTasks(); + if (disableTapsForSplitSelect) { + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mLastTouchDownPosition.set(ev.getX(), ev.getY()); + } + return super.dispatchTouchEvent(ev); + } + + /** + * @return taskId that split selection was initiated with, + * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of + * split selection + */ + protected int getThisTaskCurrentlyInSplitSelection() { + SplitSelectStateController splitSelectController = + getRecentsView().getSplitSelectController(); + int initSplitTaskId = INVALID_TASK_ID; + for (TaskIdAttributeContainer container : getTaskIdAttributeContainers()) { + int taskId = container.getTask().key.id; + if (taskId == splitSelectController.getInitialTaskId()) { + initSplitTaskId = taskId; + break; + } + } + return initSplitTaskId; + } + private void onClick(View view) { if (getTask() == null) { return; @@ -605,72 +746,7 @@ public class TaskView extends FrameLayout implements Reusable { if (confirmSecondSplitSelectApp()) { return; } - RecentsView recentsView = getRecentsView(); - RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles; - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask() && remoteTargetHandles != null) { - if (!mIsClickableAsLiveTile) { - return; - } - - // Reset the minimized state since we force-toggled the minimized state when entering - // overview, but never actually finished the recents animation - SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate(); - if (p != null) { - p.setSplitScreenMinimized(false); - } - - mIsClickableAsLiveTile = false; - RemoteAnimationTargets targets; - if (remoteTargetHandles.length == 1) { - targets = remoteTargetHandles[0].getTransformParams().getTargetSet(); - } else { - TransformParams topLeftParams = remoteTargetHandles[0].getTransformParams(); - TransformParams rightBottomParams = remoteTargetHandles[1].getTransformParams(); - RemoteAnimationTargetCompat[] apps = Stream.concat( - Arrays.stream(topLeftParams.getTargetSet().apps), - Arrays.stream(rightBottomParams.getTargetSet().apps)) - .toArray(RemoteAnimationTargetCompat[]::new); - RemoteAnimationTargetCompat[] wallpapers = Stream.concat( - Arrays.stream(topLeftParams.getTargetSet().wallpapers), - Arrays.stream(rightBottomParams.getTargetSet().wallpapers)) - .toArray(RemoteAnimationTargetCompat[]::new); - targets = new RemoteAnimationTargets(apps, wallpapers, - topLeftParams.getTargetSet().nonApps, - topLeftParams.getTargetSet().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. - launchTaskAnimated(); - return; - } - - AnimatorSet anim = new AnimatorSet(); - TaskViewUtils.composeRecentsLaunchAnimator( - anim, this, targets.apps, - targets.wallpapers, targets.nonApps, true /* launcherClosing */, - mActivity.getStateManager(), recentsView, - recentsView.getDepthController()); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - recentsView.runActionOnRemoteHandles( - (Consumer) remoteTargetHandle -> - remoteTargetHandle - .getTaskViewSimulator() - .setDrawsBelowRecents(false)); - } - - @Override - public void onAnimationEnd(Animator animator) { - mIsClickableAsLiveTile = true; - } - }); - anim.start(); - recentsView.onTaskLaunchedInLiveTileMode(); - } else { - launchTaskAnimated(); - } + launchTasks(); mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo()) .log(LAUNCHER_TASK_LAUNCH_TAP); } @@ -679,12 +755,25 @@ public class TaskView extends FrameLayout implements Reusable { * @return {@code true} if user is already in split select mode and this tap was to choose the * second app. {@code false} otherwise */ - private boolean confirmSecondSplitSelectApp() { - boolean isSelectingSecondSplitApp = getRecentsView().isSplitSelectionActive(); - if (isSelectingSecondSplitApp) { - getRecentsView().confirmSplitSelect(this); + protected boolean confirmSecondSplitSelectApp() { + int index = getLastSelectedChildTaskIndex(); + TaskIdAttributeContainer container = mTaskIdAttributeContainer[index]; + if (container != null) { + return getRecentsView().confirmSplitSelect(this, container.getTask(), + container.getIconView().getDrawable(), container.getThumbnailView(), + container.getThumbnailView().getThumbnail(), /* intent */ null, + /* user */ null); } - return isSelectingSecondSplitApp; + return false; + } + + /** + * Returns the task index of the last selected child task (0 or 1). + * If we contain multiple tasks and this TaskView is used as part of split selection, the + * selected child task index will be that of the remaining task. + */ + protected int getLastSelectedChildTaskIndex() { + return 0; } /** @@ -697,11 +786,12 @@ public class TaskView extends FrameLayout implements Reusable { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask); ActivityOptionsWrapper opts = mActivity.getActivityLaunchOptions(this, null); - opts.options.setLaunchDisplayId(getRootViewDisplayId()); + opts.options.setLaunchDisplayId( + getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId()); if (ActivityManagerWrapper.getInstance() .startActivityFromRecents(mTask.key, opts.options)) { RecentsView recentsView = getRecentsView(); - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskViewId() != -1) { + if (recentsView.getRunningTaskViewId() != -1) { recentsView.onTaskLaunchedInLiveTileMode(); // Return a fresh callback in the live tile case, so that it's not accidentally @@ -736,12 +826,14 @@ public class TaskView extends FrameLayout implements Reusable { TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask); // Indicate success once the system has indicated that the transition has started - ActivityOptions opts = ActivityOptionsCompat.makeCustomAnimation( - getContext(), 0, 0, () -> callback.accept(true), MAIN_EXECUTOR.getHandler()); - opts.setLaunchDisplayId(getRootViewDisplayId()); + ActivityOptions opts = makeCustomAnimation(getContext(), 0, 0, + () -> callback.accept(true), MAIN_EXECUTOR.getHandler()); + opts.setLaunchDisplayId( + getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId()); if (freezeTaskList) { - ActivityOptionsCompat.setFreezeRecentTasksList(opts); + opts.setFreezeRecentTasksReordering(); } + opts.setDisableStartingWindow(mSnapshotView.shouldShowSplashView()); Task.TaskKey key = mTask.key; UI_HELPER_EXECUTOR.execute(() -> { if (!ActivityManagerWrapper.getInstance().startActivityFromRecents(key, opts)) { @@ -759,6 +851,107 @@ public class TaskView extends FrameLayout implements Reusable { } } + /** + * Returns ActivityOptions for overriding task transition animation. + */ + private ActivityOptions makeCustomAnimation(Context context, int enterResId, + int exitResId, final Runnable callback, final Handler callbackHandler) { + return ActivityOptions.makeCustomTaskAnimation(context, enterResId, exitResId, + callbackHandler, + elapsedRealTime -> { + if (callback != null) { + callbackHandler.post(callback); + } + }, null /* finishedListener */); + } + + /** + * Launch of the current task (both live and inactive tasks) with an animation. + */ + @Nullable + public RunnableList launchTasks() { + RecentsView recentsView = getRecentsView(); + RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles; + if (isRunningTask() && remoteTargetHandles != null) { + if (!mIsClickableAsLiveTile) { + Log.e(TAG, "TaskView is not clickable as a live tile; returning to home."); + return null; + } + + mIsClickableAsLiveTile = false; + RemoteAnimationTargets targets; + if (remoteTargetHandles.length == 1) { + targets = remoteTargetHandles[0].getTransformParams().getTargetSet(); + } else { + TransformParams topLeftParams = remoteTargetHandles[0].getTransformParams(); + TransformParams rightBottomParams = remoteTargetHandles[1].getTransformParams(); + RemoteAnimationTarget[] apps = Stream.concat( + Arrays.stream(topLeftParams.getTargetSet().apps), + Arrays.stream(rightBottomParams.getTargetSet().apps)) + .toArray(RemoteAnimationTarget[]::new); + RemoteAnimationTarget[] wallpapers = Stream.concat( + Arrays.stream(topLeftParams.getTargetSet().wallpapers), + Arrays.stream(rightBottomParams.getTargetSet().wallpapers)) + .toArray(RemoteAnimationTarget[]::new); + targets = new RemoteAnimationTargets(apps, wallpapers, + topLeftParams.getTargetSet().nonApps, + topLeftParams.getTargetSet().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. + RunnableList runnableList = launchTaskAnimated(); + if (runnableList == null) { + Log.e(TAG, "Recents animation cancelled and cannot launch task as non-live tile" + + "; returning to home"); + } + mIsClickableAsLiveTile = true; + return runnableList; + } + + RunnableList runnableList = new RunnableList(); + AnimatorSet anim = new AnimatorSet(); + TaskViewUtils.composeRecentsLaunchAnimator( + anim, this, targets.apps, + targets.wallpapers, targets.nonApps, true /* launcherClosing */, + mActivity.getStateManager(), recentsView, + recentsView.getDepthController()); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + recentsView.runActionOnRemoteHandles( + (Consumer) remoteTargetHandle -> + remoteTargetHandle + .getTaskViewSimulator() + .setDrawsBelowRecents(false)); + } + + @Override + public void onAnimationEnd(Animator animator) { + if (mTask != null && mTask.key.displayId != getRootViewDisplayId()) { + launchTaskAnimated(); + } + mIsClickableAsLiveTile = true; + runEndCallback(); + } + + @Override + public void onAnimationCancel(Animator animation) { + runEndCallback(); + } + + private void runEndCallback() { + runnableList.executeAllAndDestroy(); + } + }); + anim.start(); + recentsView.onTaskLaunchedInLiveTileMode(); + return runnableList; + } else { + return launchTaskAnimated(); + } + } + /** * See {@link TaskDataChanges} * @param visible If this task view will be visible to the user in overview or hidden @@ -825,12 +1018,12 @@ public class TaskView extends FrameLayout implements Reusable { } private boolean showTaskMenu(IconView iconView) { - if (getRecentsView().mActivity.isInState(OVERVIEW_SPLIT_SELECT)) { + if (!getRecentsView().canLaunchFullscreenTask()) { // Don't show menu when selecting second split screen app return true; } - if (!mActivity.getDeviceProfile().overviewShowAsGrid + if (!mActivity.getDeviceProfile().isTablet && !getRecentsView().isClearAllHidden()) { getRecentsView().snapToPage(getRecentsView().indexOfChild(this)); return false; @@ -844,10 +1037,20 @@ public class TaskView extends FrameLayout implements Reusable { protected boolean showTaskMenuWithContainer(IconView iconView) { TaskIdAttributeContainer menuContainer = mTaskIdAttributeContainer[iconView == mIconView ? 0 : 1]; - if (mActivity.getDeviceProfile().overviewShowAsGrid) { - boolean alignSecondRow = getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) - && mActivity.getDeviceProfile().isLandscape; - return TaskMenuViewWithArrow.Companion.showForTask(menuContainer, alignSecondRow); + DeviceProfile dp = mActivity.getDeviceProfile(); + if (dp.isTablet) { + int alignedOptionIndex = 0; + if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) { + if (FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW.get()) { + // With no focused task, there is less available space below the tasks, so align + // the arrow to the third option in the menu. + alignedOptionIndex = 2; + } else { + // Bottom row of landscape grid aligns arrow to second option to avoid clipping + alignedOptionIndex = 1; + } + } + return TaskMenuViewWithArrow.Companion.showForTask(menuContainer, alignedOptionIndex); } else { return TaskMenuView.showForTask(menuContainer); } @@ -860,15 +1063,7 @@ public class TaskView extends FrameLayout implements Reusable { if (confirmSecondSplitSelectApp()) { return; } - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) { - RecentsView recentsView = getRecentsView(); - recentsView.switchToScreenshot( - () -> recentsView.finishRecentsAnimation(true /* toRecents */, - false /* shouldPip */, - () -> showTaskMenu(iconView))); - } else { - showTaskMenu(iconView); - } + showTaskMenu(iconView); }); iconView.setOnLongClickListener(v -> { requestDisallowInterceptTouchEvent(true); @@ -882,29 +1077,45 @@ public class TaskView extends FrameLayout implements Reusable { } public void setOrientationState(RecentsOrientedState orientationState) { + setIconOrientation(orientationState); + setThumbnailOrientation(orientationState); + } + + protected void setIconOrientation(RecentsOrientedState orientationState) { PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler(); boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; - LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - snapshotParams.topMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + boolean isGridTask = isGridTask(); - int taskIconHeight = deviceProfile.overviewTaskIconSizePx; - int taskMargin = isGridTask ? deviceProfile.overviewTaskMarginGridPx - : deviceProfile.overviewTaskMarginPx; - int taskIconMargin = snapshotParams.topMargin - taskIconHeight - taskMargin; LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); - orientationHandler.setIconAndSnapshotParams(mIconView, taskIconMargin, taskIconHeight, - snapshotParams, isRtl); - mSnapshotView.setLayoutParams(snapshotParams); + + int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + int taskIconHeight = deviceProfile.overviewTaskIconSizePx; + int taskMargin = deviceProfile.overviewTaskMarginPx; + + orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight, + thumbnailTopMargin, isRtl); iconParams.width = iconParams.height = taskIconHeight; mIconView.setLayoutParams(iconParams); + mIconView.setRotation(orientationHandler.getDegreesRotated()); int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx : deviceProfile.overviewTaskIconDrawableSizePx; mIconView.setDrawableSize(iconDrawableSize, iconDrawableSize); - snapshotParams.topMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + } + + protected void setThumbnailOrientation(RecentsOrientedState orientationState) { + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); + int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + + // TODO(b/271468547), we should default to setting trasnlations only on the snapshot instead + // of a hybrid of both margins and translations + LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); + snapshotParams.topMargin = thumbnailTopMargin; mSnapshotView.setLayoutParams(snapshotParams); + mSnapshotView.getTaskOverlay().updateOrientationState(orientationState); + mDigitalWellBeingToast.initialize(mTask); } /** @@ -912,10 +1123,19 @@ public class TaskView extends FrameLayout implements Reusable { */ public boolean isGridTask() { DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - return deviceProfile.overviewShowAsGrid && !isFocusedTask(); + return deviceProfile.isTablet && !isFocusedTask(); } - protected void setIconAndDimTransitionProgress(float progress, boolean invert) { + /** Whether this task view represents the desktop */ + public boolean isDesktopTask() { + return false; + } + + /** + * Called to animate a smooth transition when going directly from an app into Overview (and + * vice versa). Icons fade in, and DWB banners slide in with a "shift up" animation. + */ + protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { if (invert) { progress = 1 - progress; } @@ -926,9 +1146,7 @@ public class TaskView extends FrameLayout implements Reusable { float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp) .getInterpolation(progress); mIconView.setAlpha(scale); - mDigitalWellBeingToast.updateBannerOffset(1f - scale, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mDigitalWellBeingToast.updateBannerOffset(1f - scale); } public void setIconScaleAnimStartProgress(float startProgress) { @@ -959,7 +1177,7 @@ public class TaskView extends FrameLayout implements Reusable { if (mIconAndDimAnimator != null) { mIconAndDimAnimator.cancel(); } - setIconAndDimTransitionProgress(iconScale, invert); + setIconsAndBannersTransitionProgress(iconScale, invert); } protected void resetPersistentViewTransforms() { @@ -973,8 +1191,11 @@ public class TaskView extends FrameLayout implements Reusable { // resetViewTransforms is called during Quickswitch scrolling. mDismissTranslationX = mTaskOffsetTranslationX = mTaskResistanceTranslationX = mSplitSelectTranslationX = mGridEndTranslationX = 0f; - mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = - mSplitSelectTranslationY = 0f; + mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = 0f; + if (getRecentsView() == null || !getRecentsView().isSplitSelectionActive()) { + mSplitSelectTranslationY = 0f; + } + setSnapshotScale(1f); applyTranslationX(); applyTranslationY(); @@ -982,6 +1203,7 @@ public class TaskView extends FrameLayout implements Reusable { setAlpha(mStableAlpha); setIconScaleAndDim(1); setColorTint(0, 0); + mSnapshotView.resetViewTransforms(); } public void setStableAlpha(float parentAlpha) { @@ -1000,13 +1222,13 @@ public class TaskView extends FrameLayout implements Reusable { } public float getTaskCornerRadius() { - return TaskCornerRadius.get(mActivity); + return mCurrentFullscreenParams.mCornerRadius; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (mActivity.getDeviceProfile().overviewShowAsGrid) { + if (mActivity.getDeviceProfile().isTablet) { setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : right - left); setPivotY(mSnapshotView.getTop()); } else { @@ -1023,7 +1245,7 @@ public class TaskView extends FrameLayout implements Reusable { * How much to scale down pages near the edge of the screen. */ public static float getEdgeScaleDownFactor(DeviceProfile deviceProfile) { - return deviceProfile.overviewShowAsGrid ? EDGE_SCALE_DOWN_FACTOR_GRID + return deviceProfile.isTablet ? EDGE_SCALE_DOWN_FACTOR_GRID : EDGE_SCALE_DOWN_FACTOR_CAROUSEL; } @@ -1073,6 +1295,22 @@ public class TaskView extends FrameLayout implements Reusable { return scale; } + /** + * Updates alpha of task thumbnail splash on swipe up/down. + */ + public void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) { + mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha; + applyThumbnailSplashAlpha(); + } + + protected void applyThumbnailSplashAlpha() { + mSnapshotView.setSplashAlpha(mTaskThumbnailSplashAlpha); + } + + protected void refreshTaskThumbnailSplash() { + mSnapshotView.refreshSplashView(); + } + private void setSplitSelectTranslationX(float x) { mSplitSelectTranslationX = x; applyTranslationX(); @@ -1083,10 +1321,6 @@ public class TaskView extends FrameLayout implements Reusable { applyTranslationY(); } - public void setSplitScrollOffsetPrimary(float splitSelectScrollOffsetPrimary) { - mSplitSelectScrollOffsetPrimary = splitSelectScrollOffsetPrimary; - } - private void setDismissTranslationX(float x) { mDismissTranslationX = x; applyTranslationX(); @@ -1150,19 +1384,18 @@ public class TaskView extends FrameLayout implements Reusable { applyTranslationX(); } - public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) { + public float getScrollAdjustment(boolean gridEnabled) { float scrollAdjustment = 0; if (gridEnabled) { scrollAdjustment += mGridTranslationX; } else { scrollAdjustment += getPrimaryNonGridTranslationProperty().get(this); } - scrollAdjustment += mSplitSelectScrollOffsetPrimary; return scrollAdjustment; } - public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) { - return getScrollAdjustment(fullscreenEnabled, gridEnabled); + public float getOffsetAdjustment(boolean gridEnabled) { + return getScrollAdjustment(gridEnabled); } public float getSizeAdjustment(boolean fullscreenEnabled) { @@ -1221,7 +1454,7 @@ public class TaskView extends FrameLayout implements Reusable { DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y); } - public FloatProperty getSecondaryDissmissTranslationProperty() { + public FloatProperty getSecondaryDismissTranslationProperty() { return getPagedOrientationHandler().getSecondaryValue( DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y); } @@ -1231,6 +1464,11 @@ public class TaskView extends FrameLayout implements Reusable { TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y); } + public FloatProperty getSecondaryTaskOffsetTranslationProperty() { + return getPagedOrientationHandler().getSecondaryValue( + TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y); + } + public FloatProperty getTaskResistanceTranslationProperty() { return getPagedOrientationHandler().getSecondaryValue( TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y); @@ -1260,33 +1498,6 @@ public class TaskView extends FrameLayout implements Reusable { mEndQuickswitchCuj = endQuickswitchCuj; } - private static final class TaskOutlineProvider extends ViewOutlineProvider { - - private int mMarginTop; - private FullscreenDrawParams mFullscreenParams; - - TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams, int topMargin) { - mMarginTop = topMargin; - mFullscreenParams = fullscreenParams; - } - - public void updateParams(FullscreenDrawParams params, int topMargin) { - mFullscreenParams = params; - mMarginTop = topMargin; - } - - @Override - public void getOutline(View view, Outline outline) { - RectF insets = mFullscreenParams.mCurrentDrawnInsets; - float scale = mFullscreenParams.mScale; - outline.setRoundRect(0, - (int) (mMarginTop * scale), - (int) ((insets.left + view.getWidth() + insets.right) * scale), - (int) ((insets.top + view.getHeight() + insets.bottom) * scale), - mFullscreenParams.mCurrentDrawnCornerRadius); - } - } - private int getExpectedViewHeight(View view) { int expectedHeight; int h = view.getLayoutParams().height; @@ -1314,7 +1525,7 @@ public class TaskView extends FrameLayout implements Reusable { continue; } for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this, - mActivity.getDeviceProfile(), taskContainer)) { + taskContainer)) { info.addAction(s.createAccessibilityAction(context)); } } @@ -1352,7 +1563,7 @@ public class TaskView extends FrameLayout implements Reusable { continue; } for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this, - mActivity.getDeviceProfile(), taskContainer)) { + taskContainer)) { if (s.hasHandlerForAction(action)) { s.onClick(this); return true; @@ -1391,12 +1602,13 @@ public class TaskView extends FrameLayout implements Reusable { mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE); mSnapshotView.getTaskOverlay().setFullscreenProgress(progress); - updateSnapshotRadius(); + // Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are + // oversized and banner would look disproportionately large. + if (mActivity.getStateManager().getState() != BACKGROUND_APP) { + setIconsAndBannersTransitionProgress(progress, true); + } - mOutlineProvider.updateParams( - mCurrentFullscreenParams, - mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx); - invalidateOutline(); + updateSnapshotRadius(); } protected void updateSnapshotRadius() { @@ -1423,7 +1635,7 @@ public class TaskView extends FrameLayout implements Reusable { int expectedWidth; int expectedHeight; DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - if (deviceProfile.overviewShowAsGrid) { + if (deviceProfile.isTablet) { final int thumbnailPadding = deviceProfile.overviewTaskThumbnailTopMarginPx; final Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize(); final int taskWidth = lastComputedTaskSize.width(); @@ -1432,7 +1644,12 @@ public class TaskView extends FrameLayout implements Reusable { int boxWidth; int boxHeight; boolean isFocusedTask = isFocusedTask(); - if (isFocusedTask) { + if (isDesktopTask()) { + Rect lastComputedDesktopTaskSize = + getRecentsView().getLastComputedDesktopTaskSize(); + boxWidth = lastComputedDesktopTaskSize.width(); + boxHeight = lastComputedDesktopTaskSize.height(); + } else 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. boxWidth = taskWidth; @@ -1508,8 +1725,8 @@ public class TaskView extends FrameLayout implements Reusable { } public void initiateSplitSelect(SplitPositionOption splitPositionOption) { - AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_TASK_MENU); - getRecentsView().initiateSplitSelect(this, splitPositionOption.stagePosition); + getRecentsView().initiateSplitSelect(this, splitPositionOption.stagePosition, + getLogEventForPosition(splitPositionOption.stagePosition)); } /** @@ -1523,7 +1740,23 @@ public class TaskView extends FrameLayout implements Reusable { private int getRootViewDisplayId() { - return getRootView().getDisplay().getDisplayId(); + Display display = getRootView().getDisplay(); + return display != null ? display.getDisplayId() : DEFAULT_DISPLAY; + } + + /** + * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). + * IconView is unaffected. + * + * @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value + */ + void setThumbnailVisibility(int visibility, int taskId) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != mIconView) { + child.setVisibility(visibility); + } + } } /** @@ -1534,11 +1767,7 @@ public class TaskView extends FrameLayout implements Reusable { private final float mCornerRadius; private final float mWindowCornerRadius; - public float mFullscreenProgress; - public RectF mCurrentDrawnInsets = new RectF(); public float mCurrentDrawnCornerRadius; - /** The current scale we apply to the thumbnail to adjust for new left/right insets. */ - public float mScale = 1; public FullscreenDrawParams(Context context) { mCornerRadius = TaskCornerRadius.get(context); @@ -1552,29 +1781,9 @@ public class TaskView extends FrameLayout implements Reusable { */ public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale, int previewWidth, DeviceProfile dp, PreviewPositionHelper pph) { - mFullscreenProgress = fullscreenProgress; - - RectF insets = pph.getInsetsToDrawInFullscreen(dp); - - float currentInsetsLeft = insets.left * fullscreenProgress; - float currentInsetsRight = insets.right * fullscreenProgress; - float insetsBottom = insets.bottom; - if (dp.isTaskbarPresentInApps) { - insetsBottom = Math.max(0, insetsBottom - dp.taskbarSize); - } - mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress, - currentInsetsRight, insetsBottom * fullscreenProgress); - float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius; - mCurrentDrawnCornerRadius = - Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius) + Utilities.mapRange(fullscreenProgress, mCornerRadius, mWindowCornerRadius) / parentScale / taskViewScale; - - // We scaled the thumbnail to fit the content (excluding insets) within task view width. - // Now that we are drawing left/right insets again, we need to scale down to fit them. - if (previewWidth > 0) { - mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight); - } } } diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index c1b3beb475..83341cb868 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -36,13 +36,11 @@ import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.os.UserHandle; import android.text.TextUtils; -import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.BgDataModel.FixedContainerItems; @@ -111,7 +109,6 @@ public final class WidgetsPredicationUpdateTaskTest { doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); doAnswer(i -> { String pkg = i.getArgument(0); - Log.e("Hello", "Getting v " + pkg); return TextUtils.isEmpty(pkg) ? allWidgets : allWidgets.stream() .filter(a -> pkg.equals(a.provider.getPackageName())) .collect(Collectors.toList()); @@ -138,21 +135,21 @@ public final class WidgetsPredicationUpdateTaskTest { public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() throws Exception { // WHEN newPredicationTask is executed with app predication of 5 apps. - AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className", + AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", mUserHandle); - AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className", + AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1", mUserHandle); AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className", mUserHandle); - AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className", + AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1", mUserHandle); - AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className", + AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", mUserHandle); mModelHelper.executeTaskForTest( newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1))) .forEach(Runnable::run); - // THEN only 3 widgets are returned because + // THEN only 2 widgets are returned because // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are // excluded from the result. // 2. app3 doesn't have a widget. @@ -161,45 +158,39 @@ public final class WidgetsPredicationUpdateTaskTest { .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); - assertThat(recommendedWidgets).hasSize(3); + assertThat(recommendedWidgets).hasSize(2); assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1); - assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2); - assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1); + assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1); } @Test - public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder() + public void widgetsRecommendationRan_shouldReturnPackageWidgetsWhenEmpty() throws Exception { - if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) { - return; - } - // WHEN newPredicationTask is executed with 5 predicated widgets. - AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", - mUserHandle); - AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2", + // Not installed widget + AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3", mUserHandle); // Not installed app AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1", mUserHandle); - // Not installed widget - AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3", + // Workspace added widgets + AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1", mUserHandle); AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", mUserHandle); mModelHelper.executeTaskForTest( - newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1))) + newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1))) .forEach(Runnable::run); - // THEN only 3 widgets are returned because the launcher only filters out non-exist widgets. + // THEN only 2 widgets are returned because the launcher only filters out non-exist widgets. List recommendedWidgets = mCallback.mRecommendedWidgets.items .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); - assertThat(recommendedWidgets).hasSize(3); - assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1); - assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2); - assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1); + assertThat(recommendedWidgets).hasSize(2); + // Another widget from the same package + assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2); + assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1); } private void assertWidgetInfo( diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt new file mode 100644 index 0000000000..8c13fe3f1e --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.launcher3.taskbar + +import androidx.test.runner.AndroidJUnit4 +import com.android.launcher3.statemanager.StateManager +import com.android.quickstep.RecentsActivity +import com.android.quickstep.fallback.RecentsState +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() { + + lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController + lateinit var stateListener: StateManager.StateListener + + @Mock lateinit var recentsActivity: RecentsActivity + @Mock lateinit var stateManager: StateManager + + @Before + override fun setup() { + super.setup() + whenever(recentsActivity.stateManager).thenReturn(stateManager) + fallbackTaskbarUIController = FallbackTaskbarUIController(recentsActivity) + + // Capture registered state listener to send events to in our tests + val captor = ArgumentCaptor.forClass(StateManager.StateListener::class.java) + fallbackTaskbarUIController.init(taskbarControllers) + verify(stateManager).addStateListener(captor.capture()) + stateListener = captor.value as StateManager.StateListener + } + + @Test + fun stateTransitionComplete_stateDefault() { + stateListener.onStateTransitionComplete(RecentsState.DEFAULT) + // verify dragging disabled + verify(taskbarDragController, times(1)).setDisallowGlobalDrag(true) + verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(true) + // verify long click enabled + verify(taskbarDragController, times(1)).setDisallowLongClick(false) + verify(taskbarAllAppsController, times(1)).setDisallowLongClick(false) + // verify split selection enabled + verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(true) + } + + @Test + fun stateTransitionComplete_stateSplitSelect() { + stateListener.onStateTransitionComplete(RecentsState.OVERVIEW_SPLIT_SELECT) + // verify dragging disabled + verify(taskbarDragController, times(1)).setDisallowGlobalDrag(false) + verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(false) + // verify long click enabled + verify(taskbarDragController, times(1)).setDisallowLongClick(true) + verify(taskbarAllAppsController, times(1)).setDisallowLongClick(true) + // verify split selection enabled + verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(false) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java new file mode 100644 index 0000000000..929bff3004 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java @@ -0,0 +1,125 @@ +package com.android.launcher3.taskbar; + +import static android.view.MotionEvent.ACTION_DOWN; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Instrumentation; +import android.content.Context; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.View; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.DeviceProfile; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Supplier; + +@RunWith(AndroidJUnit4.class) +public class RecentsHitboxExtenderTest { + + private static final int TASKBAR_OFFSET_Y = 35; + private static final int BUTTON_WIDTH = 10; + private static final int BUTTON_HEIGHT = 10; + + private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender(); + @Mock + View mMockRecentsButton; + @Mock + View mMockRecentsParent; + @Mock + DeviceProfile mMockDeviceProfile; + @Mock + Handler mMockHandler; + Context mContext; + + float[] mRecentsCoords = new float[]{0,0}; + private final Supplier mSupplier = () -> mRecentsCoords; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = instrumentation.getContext(); + mHitboxExtender.init(mMockRecentsButton, mMockRecentsParent, mMockDeviceProfile, mSupplier, + mMockHandler); + when(mMockDeviceProfile.getTaskbarOffsetY()).thenReturn(TASKBAR_OFFSET_Y); + when(mMockRecentsButton.getContext()).thenReturn(mContext); + when(mMockRecentsButton.getWidth()).thenReturn(BUTTON_WIDTH); + when(mMockRecentsButton.getHeight()).thenReturn(BUTTON_HEIGHT); + } + + @Test + public void noRecentsButtonClick_notActive() { + mHitboxExtender.onAnimationProgressToOverview(0); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertFalse(mHitboxExtender.extendedHitboxEnabled()); + } + + @Test + public void recentsButtonClick_active() { + mHitboxExtender.onRecentsButtonClicked(); + mHitboxExtender.onAnimationProgressToOverview(0); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertTrue(mHitboxExtender.extendedHitboxEnabled()); + } + + @Test + public void homeToTaskbar_notActive() { + mHitboxExtender.onAnimationProgressToOverview(1); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertFalse(mHitboxExtender.extendedHitboxEnabled()); + } + + @Test + public void animationEndReset() { + mHitboxExtender.onRecentsButtonClicked(); + mHitboxExtender.onAnimationProgressToOverview(0); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertTrue(mHitboxExtender.extendedHitboxEnabled()); + mHitboxExtender.onAnimationProgressToOverview(1); + verify(mMockHandler, times(1)).postDelayed(any(), anyLong()); + } + + @Test + public void motionWithinHitbox() { + mHitboxExtender.onRecentsButtonClicked(); + mHitboxExtender.onAnimationProgressToOverview(0); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertTrue(mHitboxExtender.extendedHitboxEnabled()); + // Center width, past height but w/in offset bounds + MotionEvent motionEvent = getMotionEvent(ACTION_DOWN, + BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y / 2); + assertTrue(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent)); + } + + @Test + public void motionOutsideHitbox() { + mHitboxExtender.onRecentsButtonClicked(); + mHitboxExtender.onAnimationProgressToOverview(0); + mHitboxExtender.onAnimationProgressToOverview(0.5f); + assertTrue(mHitboxExtender.extendedHitboxEnabled()); + // Center width, past height and offset + MotionEvent motionEvent = getMotionEvent(ACTION_DOWN, + BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y * 2); + assertFalse(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent)); + } + + private MotionEvent getMotionEvent(int action, int x, int y) { + return MotionEvent.obtain(0, 0, action, x, y, 0); + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt new file mode 100644 index 0000000000..172cb469be --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController +import com.android.launcher3.taskbar.overlay.TaskbarOverlayController +import com.android.systemui.shared.rotation.RotationButtonController +import org.junit.Before +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +/** + * Helper class to extend to get access to all controllers. Gotta be careful of your relationship + * with this class though, it can be quite... controlling. + */ +abstract class TaskbarBaseTestCase { + + @Mock lateinit var taskbarActivityContext: TaskbarActivityContext + @Mock lateinit var taskbarDragController: TaskbarDragController + @Mock lateinit var navButtonController: TaskbarNavButtonController + @Mock lateinit var navbarButtonsViewController: NavbarButtonsViewController + @Mock lateinit var rotationButtonController: RotationButtonController + @Mock lateinit var taskbarDragLayerController: TaskbarDragLayerController + @Mock lateinit var taskbarScrimViewController: TaskbarScrimViewController + @Mock lateinit var taskbarViewController: TaskbarViewController + @Mock lateinit var taskbarUnfoldAnimationController: TaskbarUnfoldAnimationController + @Mock lateinit var taskbarKeyguardController: TaskbarKeyguardController + @Mock lateinit var stashedHandleViewController: StashedHandleViewController + @Mock lateinit var taskbarStashController: TaskbarStashController + @Mock lateinit var taskbarEduController: TaskbarEduController + @Mock lateinit var taskbarAutohideSuspendController: TaskbarAutohideSuspendController + @Mock lateinit var taskbarPopupController: TaskbarPopupController + @Mock + lateinit var taskbarForceVisibleImmersiveController: TaskbarForceVisibleImmersiveController + @Mock lateinit var taskbarAllAppsController: TaskbarAllAppsController + @Mock lateinit var taskbarInsetsController: TaskbarInsetsController + @Mock lateinit var voiceInteractionWindowController: VoiceInteractionWindowController + @Mock lateinit var taskbarRecentAppsController: TaskbarRecentAppsController + @Mock lateinit var taskbarTranslationController: TaskbarTranslationController + @Mock lateinit var taskbarSpringOnStashController: TaskbarSpringOnStashController + @Mock lateinit var taskbarOverlayController: TaskbarOverlayController + @Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController + @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController + + lateinit var taskbarControllers: TaskbarControllers + + @Before + open fun setup() { + /* + * NOTE: Mocking of controllers that are written in Kotlin won't work since their methods + * are final by default (and should not be changed only for tests), meaning unmockable. + * Womp, womp woooommmmppp. + * If you want to mock one of those methods, you need to make a parent interface that + * includes that method to allow mocking it. + */ + MockitoAnnotations.initMocks(this) + taskbarControllers = + TaskbarControllers( + taskbarActivityContext, + taskbarDragController, + navButtonController, + navbarButtonsViewController, + rotationButtonController, + taskbarDragLayerController, + taskbarViewController, + taskbarScrimViewController, + taskbarUnfoldAnimationController, + taskbarKeyguardController, + stashedHandleViewController, + taskbarStashController, + taskbarEduController, + taskbarAutohideSuspendController, + taskbarPopupController, + taskbarForceVisibleImmersiveController, + taskbarOverlayController, + taskbarAllAppsController, + taskbarInsetsController, + voiceInteractionWindowController, + taskbarTranslationController, + taskbarSpringOnStashController, + taskbarRecentAppsController, + taskbarEduTooltipController, + keyboardQuickSwitchController + ) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt new file mode 100644 index 0000000000..148e36cde5 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar + +import android.app.KeyguardManager +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +class TaskbarKeyguardControllerTest : TaskbarBaseTestCase() { + + @Mock lateinit var baseDragLayer: TaskbarDragLayer + @Mock lateinit var keyguardManager: KeyguardManager + + @Before + override fun setup() { + super.setup() + whenever(taskbarActivityContext.getSystemService(KeyguardManager::class.java)) + .thenReturn(keyguardManager) + whenever(baseDragLayer.childCount).thenReturn(0) + whenever(taskbarActivityContext.dragLayer).thenReturn(baseDragLayer) + + taskbarKeyguardController = TaskbarKeyguardController(taskbarActivityContext) + taskbarKeyguardController.init(navbarButtonsViewController) + } + + @Test + fun uninterestingFlags_noActions() { + setFlags(0) + verify(navbarButtonsViewController, never()).setKeyguardVisible(anyBoolean(), anyBoolean()) + } + + @Test + fun keyguardShowing() { + setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) + verify(navbarButtonsViewController, times(1)) + .setKeyguardVisible(true /*isKeyguardVisible*/, false /*isKeyguardOccluded*/) + } + + @Test + fun dozingShowing() { + setFlags(SYSUI_STATE_DEVICE_DOZING) + verify(navbarButtonsViewController, times(1)) + .setKeyguardVisible(true /*isKeyguardVisible*/, false /*isKeyguardOccluded*/) + } + + @Test + fun keyguardOccluded() { + setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) + verify(navbarButtonsViewController, times(1)) + .setKeyguardVisible(false /*isKeyguardVisible*/, true /*isKeyguardOccluded*/) + } + + @Test + fun keyguardOccludedAndDozing() { + setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED.or(SYSUI_STATE_DEVICE_DOZING)) + verify(navbarButtonsViewController, times(1)) + .setKeyguardVisible(true /*isKeyguardVisible*/, true /*isKeyguardOccluded*/) + } + + @Test + fun deviceInsecure_hideBackForBouncer() { + whenever(keyguardManager.isDeviceSecure).thenReturn(false) + setFlags(SYSUI_STATE_BOUNCER_SHOWING) + + verify(navbarButtonsViewController, times(1)).setBackForBouncer(false) + } + + @Test + fun deviceSecure_showBackForBouncer() { + whenever(keyguardManager.isDeviceSecure).thenReturn(true) + setFlags(SYSUI_STATE_BOUNCER_SHOWING) + + verify(navbarButtonsViewController, times(1)).setBackForBouncer(true) + } + + @Test + fun backDisabled_hideBackForBouncer() { + whenever(keyguardManager.isDeviceSecure).thenReturn(true) + setFlags(SYSUI_STATE_BACK_DISABLED.or(SYSUI_STATE_BOUNCER_SHOWING)) + + verify(navbarButtonsViewController, times(1)).setBackForBouncer(false) + } + + private fun setFlags(flags: Int) { + taskbarKeyguardController.updateStateForSysuiFlags(flags) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index ba1a60dd38..962261940c 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -1,5 +1,11 @@ package com.android.launcher3.taskbar; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS; +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_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; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME; @@ -11,14 +17,18 @@ import static com.android.quickstep.OverviewCommandHelper.TYPE_TOGGLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; +import android.view.View; import androidx.test.runner.AndroidJUnit4; +import com.android.launcher3.logging.StatsLogManager; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; @@ -42,6 +52,16 @@ public class TaskbarNavButtonControllerTest { OverviewCommandHelper mockCommandHelper; @Mock Handler mockHandler; + @Mock + StatsLogManager mockStatsLogManager; + @Mock + StatsLogManager.StatsLogger mockStatsLogger; + @Mock + TaskbarControllers mockTaskbarControllers; + @Mock + TaskbarActivityContext mockTaskbarActivityContext; + @Mock + View mockView; private TaskbarNavButtonController mNavButtonController; @@ -50,110 +70,167 @@ public class TaskbarNavButtonControllerTest { MockitoAnnotations.initMocks(this); when(mockService.getDisplayId()).thenReturn(DISPLAY_ID); when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper); + when(mockStatsLogManager.logger()).thenReturn(mockStatsLogger); + when(mockTaskbarControllers.getTaskbarActivityContext()) + .thenReturn(mockTaskbarActivityContext); + doReturn(mockStatsLogManager).when(mockTaskbarActivityContext).getStatsLogManager(); mNavButtonController = new TaskbarNavButtonController(mockService, mockSystemUiProxy, mockHandler); } @Test public void testPressBack() { - mNavButtonController.onButtonClick(BUTTON_BACK); + mNavButtonController.onButtonClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(1)).onBackPressed(); } @Test public void testPressImeSwitcher() { - mNavButtonController.onButtonClick(BUTTON_IME_SWITCH); + mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView); verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed(); } @Test public void testPressA11yShortClick() { - mNavButtonController.onButtonClick(BUTTON_A11Y); + mNavButtonController.onButtonClick(BUTTON_A11Y, mockView); verify(mockSystemUiProxy, times(1)) .notifyAccessibilityButtonClicked(DISPLAY_ID); } @Test public void testPressA11yLongClick() { - mNavButtonController.onButtonLongClick(BUTTON_A11Y); + mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView); verify(mockSystemUiProxy, times(1)).notifyAccessibilityButtonLongClicked(); } @Test - public void testLongPressHome() { - mNavButtonController.onButtonLongClick(BUTTON_HOME); + public void testLongPressHome_enabled() { + mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); + mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); verify(mockSystemUiProxy, times(1)).startAssistant(any()); } + @Test + public void testLongPressHome_disabled() { + mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); + mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); + verify(mockSystemUiProxy, never()).startAssistant(any()); + } + @Test public void testPressHome() { - mNavButtonController.onButtonClick(BUTTON_HOME); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); verify(mockCommandHelper, times(1)).addCommand(TYPE_HOME); } @Test public void testPressRecents() { - mNavButtonController.onButtonClick(BUTTON_RECENTS); + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); verify(mockCommandHelper, times(1)).addCommand(TYPE_TOGGLE); } @Test public void testPressRecentsWithScreenPinned() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); - mNavButtonController.onButtonClick(BUTTON_RECENTS); + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); verify(mockCommandHelper, times(0)).addCommand(TYPE_TOGGLE); } @Test public void testLongPressBackRecentsNotPinned() { - mNavButtonController.onButtonLongClick(BUTTON_RECENTS); - mNavButtonController.onButtonLongClick(BUTTON_BACK); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(0)).stopScreenPinning(); } @Test public void testLongPressBackRecentsPinned() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); - mNavButtonController.onButtonLongClick(BUTTON_RECENTS); - mNavButtonController.onButtonLongClick(BUTTON_BACK); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(1)).stopScreenPinning(); } @Test public void testLongPressBackRecentsTooLongPinned() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); - mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); try { Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5); } catch (InterruptedException e) { e.printStackTrace(); } - mNavButtonController.onButtonLongClick(BUTTON_BACK); + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(0)).stopScreenPinning(); } @Test public void testLongPressBackRecentsMultipleAttemptPinned() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); - mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); try { Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5); } catch (InterruptedException e) { e.printStackTrace(); } - mNavButtonController.onButtonLongClick(BUTTON_BACK); + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(0)).stopScreenPinning(); // Try again w/in threshold - mNavButtonController.onButtonLongClick(BUTTON_RECENTS); - mNavButtonController.onButtonLongClick(BUTTON_BACK); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); verify(mockSystemUiProxy, times(1)).stopScreenPinning(); } @Test public void testLongPressHomeScreenPinned() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); - mNavButtonController.onButtonLongClick(BUTTON_HOME); + mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); verify(mockSystemUiProxy, times(0)).startAssistant(any()); } + + @Test + public void testNoCallsToNullLogger() { + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockStatsLogManager, times(0)).logger(); + verify(mockStatsLogger, times(0)).log(any()); + } + + @Test + public void testNoCallsAfterNullingOut() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + mNavButtonController.onDestroy(); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); + verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); + } + + @Test + public void testLogOnTap() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); + verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); + } + + @Test + public void testLogOnLongpress() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); + verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); + } + + @Test + public void testBackOverviewLogOnLongpress() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS); + verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP); + + mNavButtonController.onButtonLongClick(BUTTON_BACK, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); + verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); + } } diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt new file mode 100644 index 0000000000..236b5db10c --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt @@ -0,0 +1,172 @@ +package com.android.launcher3.taskbar.navbutton + +import android.content.res.Resources +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.test.runner.AndroidJUnit4 +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarManager +import java.lang.IllegalStateException +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class NavButtonLayoutFactoryTest { + + @Mock lateinit var mockDeviceProfile: DeviceProfile + @Mock lateinit var mockParentButtonContainer: FrameLayout + @Mock lateinit var mockNavLayout: LinearLayout + @Mock lateinit var mockStartContextualLayout: ViewGroup + @Mock lateinit var mockEndContextualLayout: ViewGroup + @Mock lateinit var mockResources: Resources + @Mock lateinit var mockBackButton: ImageView + @Mock lateinit var mockRecentsButton: ImageView + @Mock lateinit var mockHomeButton: ImageView + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + // Init end nav buttons + whenever(mockNavLayout.childCount).thenReturn(3) + whenever(mockNavLayout.findViewById(R.id.back)).thenReturn(mockBackButton) + whenever(mockNavLayout.findViewById(R.id.home)).thenReturn(mockHomeButton) + whenever(mockNavLayout.findViewById(R.id.recent_apps)).thenReturn(mockRecentsButton) + + // Init top level layout + whenever(mockParentButtonContainer.findViewById(R.id.end_nav_buttons)) + .thenReturn(mockNavLayout) + whenever(mockParentButtonContainer.findViewById(R.id.end_contextual_buttons)) + .thenReturn(mockEndContextualLayout) + whenever(mockParentButtonContainer.findViewById(R.id.start_contextual_buttons)) + .thenReturn(mockStartContextualLayout) + } + + @Test + fun getKidsLayoutter() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = true + val layoutter: NavButtonLayoutFactory.NavButtonLayoutter = + getLayoutter( + isKidsMode = true, + isInSetup = false, + isThreeButtonNav = false, + phoneMode = false + ) + assert(layoutter is KidsNavLayoutter) + } + + @Test + fun getSetupLayoutter() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = true + val layoutter: NavButtonLayoutFactory.NavButtonLayoutter = + getLayoutter( + isKidsMode = false, + isInSetup = true, + isThreeButtonNav = false, + phoneMode = false + ) + assert(layoutter is SetupNavLayoutter) + } + + @Test + fun getTaskbarNavLayoutter() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = true + val layoutter: NavButtonLayoutFactory.NavButtonLayoutter = + getLayoutter( + isKidsMode = false, + isInSetup = false, + isThreeButtonNav = false, + phoneMode = false + ) + assert(layoutter is TaskbarNavLayoutter) + } + + @Test(expected = IllegalStateException::class) + fun noValidLayoutForLargeScreenTaskbarNotPresent() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = false + getLayoutter( + isKidsMode = false, + isInSetup = false, + isThreeButtonNav = false, + phoneMode = false + ) + } + + @Test + fun getTaskbarPortraitLayoutter() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = false + val layoutter: NavButtonLayoutFactory.NavButtonLayoutter = + getLayoutter( + isKidsMode = false, + isInSetup = false, + isThreeButtonNav = true, + phoneMode = true + ) + assert(layoutter is PhonePortraitNavLayoutter) + } + + @Test + fun getTaskbarLandscapeLayoutter() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = false + setDeviceProfileLandscape() + val layoutter: NavButtonLayoutFactory.NavButtonLayoutter = + getLayoutter( + isKidsMode = false, + isInSetup = false, + isThreeButtonNav = true, + phoneMode = true + ) + assert(layoutter is PhoneLandscapeNavLayoutter) + } + + @Test(expected = IllegalStateException::class) + fun noValidLayoutForPhoneGestureNav() { + assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) + mockDeviceProfile.isTaskbarPresent = false + getLayoutter( + isKidsMode = false, + isInSetup = false, + isThreeButtonNav = false, + phoneMode = true + ) + } + + private fun setDeviceProfileLandscape() { + // Use reflection to modify landscape field + val landscapeField = mockDeviceProfile.javaClass.getDeclaredField("isLandscape") + landscapeField.isAccessible = true + landscapeField.set(mockDeviceProfile, true) + } + + private fun getLayoutter( + isKidsMode: Boolean, + isInSetup: Boolean, + isThreeButtonNav: Boolean, + phoneMode: Boolean + ): NavButtonLayoutFactory.NavButtonLayoutter { + return NavButtonLayoutFactory.getUiLayoutter( + deviceProfile = mockDeviceProfile, + navButtonsView = mockParentButtonContainer, + resources = mockResources, + isKidsMode = isKidsMode, + isInSetup = isInSetup, + isThreeButtonNav = isThreeButtonNav, + phoneMode = phoneMode + ) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java index 189dff8c5f..2c5825fd0b 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java @@ -16,10 +16,10 @@ package com.android.quickstep; -import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; - import static org.junit.Assert.assertTrue; +import android.os.SystemProperties; + import com.android.launcher3.Launcher; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType; @@ -33,10 +33,13 @@ import org.junit.rules.TestRule; * Base class for all instrumentation tests that deal with Quickstep. */ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { + public static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); @Override protected TestRule getRulesInsideActivityMonitor() { return RuleChain. outerRule(new NavigationModeSwitchRule(mLauncher)). + around(new TaskbarModeSwitchRule(mLauncher)). around(super.getRulesInsideActivityMonitor()); } @@ -51,7 +54,7 @@ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { @Override protected void checkLauncherState(Launcher launcher, ContainerType expectedContainerType, boolean isResumed, boolean isStarted) { - if (!isInLiveTileMode(launcher, expectedContainerType)) { + if (ENABLE_SHELL_TRANSITIONS || !isInLiveTileMode(launcher, expectedContainerType)) { super.checkLauncherState(launcher, expectedContainerType, isResumed, isStarted); } else { assertTrue("[Live Tile] hasBeenResumed() == isStarted(), hasBeenResumed(): " @@ -62,7 +65,7 @@ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { @Override protected void checkLauncherStateInOverview(Launcher launcher, ContainerType expectedContainerType, boolean isStarted, boolean isResumed) { - if (!isInLiveTileMode(launcher, expectedContainerType)) { + if (ENABLE_SHELL_TRANSITIONS || !isInLiveTileMode(launcher, expectedContainerType)) { super.checkLauncherStateInOverview(launcher, expectedContainerType, isStarted, isResumed); } else { @@ -75,8 +78,7 @@ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { private boolean isInLiveTileMode(Launcher launcher, LauncherInstrumentation.ContainerType expectedContainerType) { - if (!ENABLE_QUICKSTEP_LIVE_TILE.get() - || expectedContainerType != LauncherInstrumentation.ContainerType.OVERVIEW) { + if (expectedContainerType != LauncherInstrumentation.ContainerType.OVERVIEW) { return false; } diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java index 3e84a76695..1129a337e3 100644 --- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java +++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java @@ -46,9 +46,11 @@ public class DigitalWellBeingToastTest extends AbstractQuickStepTest { runWithShellPermission(() -> usageStatsManager.registerAppUsageLimitObserver(observerId, packages, Duration.ofSeconds(600), Duration.ofSeconds(300), - PendingIntent.getActivity(mTargetContext, -1, new Intent(), 0))); + PendingIntent.getActivity(mTargetContext, -1, new Intent() + .setPackage(mTargetContext.getPackageName()), + PendingIntent.FLAG_MUTABLE))); - mLauncher.pressHome(); + mLauncher.goHome(); final DigitalWellBeingToast toast = getToast(); waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit()); @@ -58,7 +60,7 @@ public class DigitalWellBeingToastTest extends AbstractQuickStepTest { runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); - mLauncher.pressHome(); + mLauncher.goHome(); assertFalse("Toast is visible", getToast().hasLimit()); } finally { runWithShellPermission( diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java index cba48334d8..62d46d3d8e 100644 --- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -32,6 +32,8 @@ import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification; import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand; +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.assertNotNull; @@ -57,10 +59,14 @@ import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.testcomponent.TestCommandReceiver; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.FailureWatcher; +import com.android.launcher3.util.rule.SamplerRule; +import com.android.launcher3.util.rule.ScreenRecordRule; +import com.android.launcher3.util.rule.TestStabilityRule; import com.android.quickstep.views.RecentsView; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -77,6 +83,8 @@ import java.util.function.Function; @RunWith(AndroidJUnit4.class) public class FallbackRecentsTest { + private static final String FALLBACK_LAUNCHER_TITLE = "Test launcher"; + private final UiDevice mDevice; private final LauncherInstrumentation mLauncher; private final ActivityInfo mOtherLauncherActivity; @@ -90,6 +98,9 @@ public class FallbackRecentsTest { @Rule public final TestRule mOrderSensitiveRules; + @Rule + public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule(); + public FallbackRecentsTest() throws RemoteException { Instrumentation instrumentation = getInstrumentation(); Context context = instrumentation.getContext(); @@ -105,7 +116,8 @@ public class FallbackRecentsTest { } mOrderSensitiveRules = RuleChain - .outerRule(new NavigationModeSwitchRule(mLauncher)) + .outerRule(new SamplerRule()) + .around(new NavigationModeSwitchRule(mLauncher)) .around(new FailureWatcher(mDevice, mLauncher)); mOtherLauncherActivity = context.getPackageManager().queryIntentActivities( @@ -159,22 +171,24 @@ public class FallbackRecentsTest { // b/143488140 //@NavigationModeSwitch + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/266606727 @Test public void goToOverviewFromHome() { mDevice.pressHome(); assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg( - mOtherLauncherActivity.packageName)), WAIT_TIME_MS)); + mOtherLauncherActivity.packageName).text(FALLBACK_LAUNCHER_TITLE)), WAIT_TIME_MS)); - mLauncher.getBackground().switchToOverview(); + mLauncher.getLaunchedAppState().switchToOverview(); } // b/143488140 //@NavigationModeSwitch + @Ignore @Test public void goToOverviewFromApp() { startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - mLauncher.getBackground().switchToOverview(); + mLauncher.getLaunchedAppState().switchToOverview(); } protected void executeOnRecents(Consumer f) { @@ -200,11 +214,12 @@ public class FallbackRecentsTest { private BaseOverview pressHomeAndGoToOverview() { mDevice.pressHome(); - return mLauncher.getBackground().switchToOverview(); + return mLauncher.getLaunchedAppState().switchToOverview(); } // b/143488140 //@NavigationModeSwitch + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/266606727 @Test public void testOverview() { startAppFast(getAppPackageName()); @@ -213,7 +228,7 @@ public class FallbackRecentsTest { Wait.atMost("Expected three apps in the task list", () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher); - BaseOverview overview = mLauncher.getBackground().switchToOverview(); + BaseOverview overview = mLauncher.getLaunchedAppState().switchToOverview(); executeOnRecents(recents -> { assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3); }); @@ -252,7 +267,7 @@ public class FallbackRecentsTest { // Test dismissing all tasks. pressHomeAndGoToOverview().dismissAllTasks(); assertTrue("Fallback Launcher not visible", TestHelpers.wait(Until.hasObject(By.pkg( - mOtherLauncherActivity.packageName)), WAIT_TIME_MS)); + mOtherLauncherActivity.packageName).text(FALLBACK_LAUNCHER_TITLE)), WAIT_TIME_MS)); } private int getCurrentOverviewPage(RecentsActivity recents) { diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt new file mode 100644 index 0000000000..a9dc043535 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.FakeInvariantDeviceProfileTest +import com.android.quickstep.util.TaskCornerRadius +import com.android.quickstep.views.TaskView.FullscreenDrawParams +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.system.QuickStepContract +import com.google.common.truth.Truth.assertThat +import kotlin.math.roundToInt +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +/** Test for FullscreenDrawParams class. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { + + private val TASK_SCALE = 0.7f + private var mThumbnailData: ThumbnailData = mock(ThumbnailData::class.java) + + private val mPreviewPositionHelper = PreviewPositionHelper() + private lateinit var params: FullscreenDrawParams + + @Before + fun setup() { + params = FullscreenDrawParams(context) + } + + @Test + fun setStartProgress_correctCornerRadiusForTablet() { + initializeVarsForTablet() + val dp = newDP() + val previewRect = Rect(0, 0, 100, 100) + val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() + val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() + val currentRotation = 0 + val isRtl = false + + mPreviewPositionHelper.updateThumbnailMatrix( + previewRect, + mThumbnailData, + canvasWidth, + canvasHeight, + dp.widthPx, + dp.heightPx, + dp.taskbarHeight, + dp.isTablet, + currentRotation, + isRtl + ) + params.setProgress( + /* fullscreenProgress= */ 0f, + /* parentScale= */ 1.0f, + /* taskViewScale= */ 1.0f, + /* previewWidth= */ 0, + dp, + mPreviewPositionHelper + ) + + val expectedRadius = TaskCornerRadius.get(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + } + + @Test + fun setFullProgress_correctCornerRadiusForTablet() { + initializeVarsForTablet() + val dp = newDP() + val previewRect = Rect(0, 0, 100, 100) + val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() + val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() + val currentRotation = 0 + val isRtl = false + + mPreviewPositionHelper.updateThumbnailMatrix( + previewRect, + mThumbnailData, + canvasWidth, + canvasHeight, + dp.widthPx, + dp.heightPx, + dp.taskbarHeight, + dp.isTablet, + currentRotation, + isRtl + ) + params.setProgress( + /* fullscreenProgress= */ 1.0f, + /* parentScale= */ 1.0f, + /* taskViewScale= */ 1.0f, + /* previewWidth= */ 0, + dp, + mPreviewPositionHelper + ) + + val expectedRadius = QuickStepContract.getWindowCornerRadius(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + } + + @Test + fun setStartProgress_correctCornerRadiusForPhone() { + initializeVarsForPhone() + val dp = newDP() + val previewRect = Rect(0, 0, 100, 100) + val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() + val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() + val currentRotation = 0 + val isRtl = false + + mPreviewPositionHelper.updateThumbnailMatrix( + previewRect, + mThumbnailData, + canvasWidth, + canvasHeight, + dp.widthPx, + dp.heightPx, + dp.taskbarHeight, + dp.isTablet, + currentRotation, + isRtl + ) + params.setProgress( + /* fullscreenProgress= */ 0f, + /* parentScale= */ 1.0f, + /* taskViewScale= */ 1.0f, + /* previewWidth= */ 0, + dp, + mPreviewPositionHelper + ) + + val expectedRadius = TaskCornerRadius.get(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + } + + @Test + fun setFullProgress_correctCornerRadiusForPhone() { + initializeVarsForPhone() + val dp = newDP() + val previewRect = Rect(0, 0, 100, 100) + val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() + val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() + val currentRotation = 0 + val isRtl = false + + mPreviewPositionHelper.updateThumbnailMatrix( + previewRect, + mThumbnailData, + canvasWidth, + canvasHeight, + dp.widthPx, + dp.heightPx, + dp.taskbarHeight, + dp.isTablet, + currentRotation, + isRtl + ) + params.setProgress( + /* fullscreenProgress= */ 1.0f, + /* parentScale= */ 1.0f, + /* taskViewScale= */ 1.0f, + /* previewWidth= */ 0, + dp, + mPreviewPositionHelper + ) + + val expectedRadius = QuickStepContract.getWindowCornerRadius(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt new file mode 100644 index 0000000000..a347156769 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.FakeInvariantDeviceProfileTest +import com.android.launcher3.util.WindowBounds +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { + + /** + * This is a case when after setting the hotseat, the space needs to be recalculated but it + * doesn't need to change QSB width or remove icons + */ + @Test + fun distribute_border_space_when_space_is_enough_portrait() { + initializeVarsForTablet(isGestureMode = false) + windowBounds = WindowBounds(Rect(0, 0, 1800, 2560), Rect(0, 104, 0, 0)) + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(510) + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + assertThat(dp.hotseatBorderSpace).isEqualTo(70) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(150) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(580) + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.hotseatQsbWidth).isEqualTo(1445) + } + + /** + * This is a case when after setting the hotseat, and recalculating spaces it still needs to + * remove icons for everything to fit + */ + @Test + fun decrease_num_of_icons_when_not_enough_space_portrait() { + initializeVarsForTablet(isGestureMode = false) + windowBounds = WindowBounds(Rect(0, 0, 1300, 2560), Rect(0, 104, 0, 0)) + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(510) + assertThat(dp.numShownHotseatIcons).isEqualTo(4) + assertThat(dp.hotseatBorderSpace).isEqualTo(40) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(150) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(550) + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.hotseatQsbWidth).isEqualTo(1080) + } + + /** + * This is a case when after setting the hotseat, the space needs to be recalculated but it + * doesn't need to change QSB width or remove icons + */ + @Test + fun distribute_border_space_when_space_is_enough_landscape() { + initializeVarsForTwoPanel(isGestureMode = false, isLandscape = true) + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(705) + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + assertThat(dp.hotseatBorderSpace).isEqualTo(54) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(231) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(759) + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.hotseatQsbWidth).isEqualTo(1468) + } + + /** + * This is a case when the hotseat spans a certain amount of columns and the nav buttons push + * the hotseat to the side, but not enough to change the border space. + */ + @Test + fun nav_buttons_dont_interfere_with_required_hotseat_width() { + initializeVarsForTablet(isGestureMode = false, isLandscape = true) + inv?.apply { + hotseatColumnSpan = IntArray(4) { 4 } + inlineQsb = BooleanArray(4) { false } + } + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + assertThat(dp.hotseatBorderSpace).isEqualTo(100) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(300) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(1040) + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.hotseatQsbWidth).isEqualTo(1233) + } + + /** This is a case when after setting the hotseat, the QSB width needs to be changed to fit */ + @Test + fun decrease_qsb_when_not_enough_space_landscape() { + initializeVarsForTablet(isGestureMode = false, isLandscape = true) + windowBounds = WindowBounds(Rect(0, 0, 2460, 1600), Rect(0, 104, 0, 0)) + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + assertThat(dp.hotseatBorderSpace).isEqualTo(36) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(864) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(696) + + assertThat(dp.isQsbInline).isTrue() + assertThat(dp.hotseatQsbWidth).isEqualTo(528) + } + + /** + * This is a case when after setting the hotseat, changing QSB width, and recalculating spaces + * it still needs to remove icons for everything to fit + */ + @Test + fun decrease_num_of_icons_when_not_enough_space_landscape() { + initializeVarsForTablet(isGestureMode = false, isLandscape = true) + windowBounds = WindowBounds(Rect(0, 0, 2260, 1600), Rect(0, 104, 0, 0)) + val dp = newDP() + dp.isTaskbarPresentInApps = true + + assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.numShownHotseatIcons).isEqualTo(5) + assertThat(dp.hotseatBorderSpace).isEqualTo(36) + + assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(816) + assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(700) + + assertThat(dp.isQsbInline).isTrue() + assertThat(dp.hotseatQsbWidth).isEqualTo(480) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java index 8d489e3d69..eded1c961f 100644 --- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java +++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java @@ -20,9 +20,7 @@ import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ALL; import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON; -import static com.android.quickstep.NavigationModeSwitchRule.Mode.TWO_BUTTON; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON; -import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_2BUTTON_OVERLAY; import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY; import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY; @@ -35,6 +33,7 @@ import androidx.test.uiautomator.UiDevice; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.FailureWatcher; import com.android.systemui.shared.system.QuickStepContract; @@ -52,7 +51,7 @@ import java.util.concurrent.TimeUnit; /** * Test rule that allows executing a test with Quickstep on and then Quickstep off. - * The test should be annotated with @QuickstepOnOff. + * The test should be annotated with @NavigationModeSwitch. */ public class NavigationModeSwitchRule implements TestRule { @@ -61,7 +60,7 @@ public class NavigationModeSwitchRule implements TestRule { public static final int WAIT_TIME_MS = 10000; public enum Mode { - THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL + THREE_BUTTON, ZERO_BUTTON, ALL } // Annotation for tests that need to be run with quickstep enabled and disabled. @@ -73,8 +72,8 @@ public class NavigationModeSwitchRule implements TestRule { private final LauncherInstrumentation mLauncher; - static final SysUINavigationMode SYS_UI_NAVIGATION_MODE = - SysUINavigationMode.INSTANCE.get(getInstrumentation().getTargetContext()); + static final DisplayController DISPLAY_CONTROLLER = + DisplayController.INSTANCE.get(getInstrumentation().getTargetContext()); public NavigationModeSwitchRule(LauncherInstrumentation launcher) { mLauncher = launcher; @@ -99,9 +98,6 @@ public class NavigationModeSwitchRule implements TestRule { if (mode == ZERO_BUTTON || mode == ALL) { evaluateWithZeroButtons(); } - if (mode == TWO_BUTTON || mode == ALL) { - evaluateWithTwoButtons(); - } if (mode == THREE_BUTTON || mode == ALL) { evaluateWithThreeButtons(); } @@ -123,13 +119,6 @@ public class NavigationModeSwitchRule implements TestRule { } } - private void evaluateWithTwoButtons() throws Throwable { - if (setActiveOverlay(mLauncher, NAV_BAR_MODE_2BUTTON_OVERLAY, - LauncherInstrumentation.NavigationModel.TWO_BUTTON, description)) { - base.evaluate(); - } - } - private void evaluateWithZeroButtons() throws Throwable { if (setActiveOverlay(mLauncher, NAV_BAR_MODE_GESTURAL_OVERLAY, LauncherInstrumentation.NavigationModel.ZERO_BUTTON, description)) { @@ -145,14 +134,12 @@ public class NavigationModeSwitchRule implements TestRule { public static String getCurrentOverlayPackage(int currentInteractionMode) { return QuickStepContract.isGesturalMode(currentInteractionMode) ? NAV_BAR_MODE_GESTURAL_OVERLAY - : QuickStepContract.isSwipeUpMode(currentInteractionMode) - ? NAV_BAR_MODE_2BUTTON_OVERLAY - : NAV_BAR_MODE_3BUTTON_OVERLAY; + : NAV_BAR_MODE_3BUTTON_OVERLAY; } private static LauncherInstrumentation.NavigationModel currentSysUiNavigationMode() { return LauncherInstrumentation.getNavigationModel( - SysUINavigationMode.getMode( + DisplayController.getNavigationMode( getInstrumentation(). getTargetContext()). resValue); @@ -173,18 +160,18 @@ public class NavigationModeSwitchRule implements TestRule { if (currentSysUiNavigationMode() != expectedMode) { final CountDownLatch latch = new CountDownLatch(1); final Context targetContext = getInstrumentation().getTargetContext(); - final SysUINavigationMode.NavigationModeChangeListener listener = - newMode -> { - if (LauncherInstrumentation.getNavigationModel(newMode.resValue) + final DisplayController.DisplayInfoChangeListener listener = + (context, info, flags) -> { + if (LauncherInstrumentation.getNavigationModel(info.navigationMode.resValue) == expectedMode) { latch.countDown(); } }; targetContext.getMainExecutor().execute(() -> - SYS_UI_NAVIGATION_MODE.addModeChangeListener(listener)); + DISPLAY_CONTROLLER.addChangeListener(listener)); latch.await(60, TimeUnit.SECONDS); targetContext.getMainExecutor().execute(() -> - SYS_UI_NAVIGATION_MODE.removeModeChangeListener(listener)); + DISPLAY_CONTROLLER.removeChangeListener(listener)); assertTrue(launcher, "Navigation mode didn't change to " + expectedMode, currentSysUiNavigationMode() == expectedMode, description); @@ -220,7 +207,7 @@ public class NavigationModeSwitchRule implements TestRule { if (!condition) { final AssertionError assertionError = new AssertionError(message); if (description != null) { - FailureWatcher.onError(launcher.getDevice(), description, assertionError); + FailureWatcher.onError(launcher, description, assertionError); } throw assertionError; } diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java index 159a51f733..fb18ecdcb6 100644 --- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java +++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java @@ -17,36 +17,35 @@ package com.android.quickstep; -import static android.view.Display.DEFAULT_DISPLAY; - import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; +import static com.android.launcher3.util.NavigationMode.NO_BUTTON; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import android.content.Context; import android.content.res.Resources; import android.graphics.Point; -import android.hardware.display.DisplayManager; +import android.graphics.Rect; +import android.util.ArrayMap; import android.util.DisplayMetrics; -import android.view.Display; +import android.util.Size; import android.view.MotionEvent; import android.view.Surface; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.launcher3.ResourceUtils; import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.RotationUtils; +import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.window.CachedDisplayInfo; +import com.android.launcher3.util.window.WindowManagerProxy; import org.junit.Before; import org.junit.Test; @@ -56,18 +55,9 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class OrientationTouchTransformerTest { - static class ScreenSize { - int mHeight; - int mWidth; - ScreenSize(int height, int width) { - mHeight = height; - mWidth = width; - } - } - - private static final ScreenSize NORMAL_SCREEN_SIZE = new ScreenSize(2280, 1080); - private static final ScreenSize LARGE_SCREEN_SIZE = new ScreenSize(3280, 1080); + private static final Size NORMAL_SCREEN_SIZE = new Size(1080, 2280); + private static final Size LARGE_SCREEN_SIZE = new Size(1080, 3280); private static final float DENSITY_DISPLAY_METRICS = 3.0f; private OrientationTouchTransformer mTouchTransformer; @@ -75,7 +65,6 @@ public class OrientationTouchTransformerTest { Resources mResources; private DisplayController.Info mInfo; - @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -296,33 +285,26 @@ public class OrientationTouchTransformerTest { assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY())); } - private DisplayController.Info createDisplayInfo(ScreenSize screenSize, int rotation) { - Context context = getApplicationContext(); - Display display = spy(context.getSystemService(DisplayManager.class) - .getDisplay(DEFAULT_DISPLAY)); - - Point p = new Point(screenSize.mWidth, screenSize.mHeight); - if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { - p.set(screenSize.mHeight, screenSize.mWidth); - } - - doReturn(rotation).when(display).getRotation(); - doAnswer(i -> { - ((Point) i.getArgument(0)).set(p.x, p.y); - return null; - }).when(display).getRealSize(any(Point.class)); - doAnswer(i -> { - ((Point) i.getArgument(0)).set(p.x, p.y); - ((Point) i.getArgument(1)).set(p.x, p.y); - return null; - }).when(display).getCurrentSizeRange(any(Point.class), any(Point.class)); - return new DisplayController.Info(context, display); + private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) { + Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight()); + RotationUtils.rotateSize(displaySize, rotation); + CachedDisplayInfo cachedDisplayInfo = new CachedDisplayInfo(displaySize, rotation); + WindowBounds windowBounds = new WindowBounds( + new Rect(0, 0, displaySize.x, displaySize.y), + new Rect()); + WindowManagerProxy wmProxy = mock(WindowManagerProxy.class); + doReturn(cachedDisplayInfo).when(wmProxy).getDisplayInfo(any()); + doReturn(windowBounds).when(wmProxy).getRealBounds(any(), any()); + ArrayMap internalDisplayBounds = new ArrayMap<>(); + doReturn(internalDisplayBounds).when(wmProxy).estimateInternalDisplayBounds(any()); + return new DisplayController.Info( + getApplicationContext(), wmProxy, new ArrayMap<>()); } - private float generateTouchRegionHeight(ScreenSize screenSize, int rotation) { - float height = screenSize.mHeight; + private float generateTouchRegionHeight(Size screenSize, int rotation) { + float height = screenSize.getHeight(); if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { - height = screenSize.mWidth; + height = screenSize.getWidth(); } return height - ResourceUtils.DEFAULT_NAVBAR_VALUE * DENSITY_DISPLAY_METRICS; } diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java index 4e497163b5..ed5526f410 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java +++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java @@ -26,12 +26,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.KeyguardManager; import androidx.test.filters.SmallTest; import com.android.launcher3.util.LooperExecutor; import com.android.quickstep.util.GroupTask; -import com.android.systemui.shared.system.KeyguardManagerCompat; import com.android.wm.shell.util.GroupedRecentTaskInfo; import org.junit.Before; @@ -56,8 +56,8 @@ public class RecentTasksListTest { public void setup() { MockitoAnnotations.initMocks(this); LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class); - KeyguardManagerCompat mockKeyguardManagerCompat = mock(KeyguardManagerCompat.class); - mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManagerCompat, + KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); + mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager, mockSystemUiProxy); } @@ -70,7 +70,7 @@ public class RecentTasksListTest { @Test public void loadTasksInBackground_onlyKeys_noValidTaskDescription() { - GroupedRecentTaskInfo recentTaskInfos = new GroupedRecentTaskInfo( + GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks( new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null); when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); @@ -90,8 +90,8 @@ public class RecentTasksListTest { task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); ActivityManager.RecentTaskInfo task2 = new ActivityManager.RecentTaskInfo(); task2.taskDescription = new ActivityManager.TaskDescription(); - GroupedRecentTaskInfo recentTaskInfos = new GroupedRecentTaskInfo( - task1, task2, null); + GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2, + null); when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java index f44a81275f..df5303f4f4 100644 --- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java @@ -16,20 +16,12 @@ package com.android.quickstep; -import static com.android.launcher3.util.RaceConditionReproducer.enterEvt; -import static com.android.launcher3.util.RaceConditionReproducer.exitEvt; - -import android.content.Intent; - import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import com.android.launcher3.Launcher; import com.android.launcher3.ui.TaplTestsLauncher3; import com.android.launcher3.util.RaceConditionReproducer; -import com.android.quickstep.NavigationModeSwitchRule.Mode; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; -import com.android.quickstep.inputconsumers.OtherActivityInputConsumer; import org.junit.Before; import org.junit.Ignore; @@ -48,9 +40,9 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { super.setUp(); TaplTestsLauncher3.initialize(this); // b/143488140 - mLauncher.pressHome(); + mLauncher.goHome(); // Start an activity where the gestures start. - startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); + startTestActivity(2); } private void runTest(String... eventSequence) { @@ -61,25 +53,11 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { // The test action. eventProcessor.startIteration(); - mLauncher.pressHome(); + mLauncher.goHome(); eventProcessor.finishIteration(); } - @Test - @Ignore // Ignoring until race condition repro framework is changes for multi-process case. - @NavigationModeSwitch(mode = Mode.TWO_BUTTON) - public void testPressHome() { - runTest(enterEvt(Launcher.ON_CREATE_EVT), - exitEvt(Launcher.ON_CREATE_EVT), - enterEvt(OtherActivityInputConsumer.DOWN_EVT), - exitEvt(OtherActivityInputConsumer.DOWN_EVT)); - - runTest(enterEvt(OtherActivityInputConsumer.DOWN_EVT), - exitEvt(OtherActivityInputConsumer.DOWN_EVT), - enterEvt(Launcher.ON_CREATE_EVT), - exitEvt(Launcher.ON_CREATE_EVT)); - } - + @Ignore @Test @NavigationModeSwitch public void testStressPressHome() { @@ -88,10 +66,11 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { closeLauncherActivity(); // The test action. - mLauncher.pressHome(); + mLauncher.goHome(); } } + @Ignore @Test @NavigationModeSwitch public void testStressSwipeToOverview() { @@ -100,9 +79,9 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { closeLauncherActivity(); // The test action. - mLauncher.getBackground().switchToOverview(); + mLauncher.getLaunchedAppState().switchToOverview(); } closeLauncherActivity(); - mLauncher.pressHome(); + mLauncher.goHome(); } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index 1262109a5a..bc5fa19ce1 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -21,6 +21,7 @@ import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import android.content.Intent; @@ -31,13 +32,11 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.tapl.AllApps; -import com.android.launcher3.tapl.Background; +import com.android.launcher3.tapl.LaunchedAppState; import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel; import com.android.launcher3.tapl.Overview; import com.android.launcher3.tapl.OverviewActions; import com.android.launcher3.tapl.OverviewTask; -import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.ui.TaplTestsLauncher3; import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; @@ -54,6 +53,8 @@ import org.junit.runner.RunWith; public class TaplTestsQuickstep extends AbstractQuickStepTest { private static final String APP_NAME = "LauncherTestApp"; + private static final String CALCULATOR_APP_PACKAGE = + resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); @Before public void setUp() throws Exception { @@ -75,7 +76,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public static void startTestApps() throws Exception { startAppFast(getAppPackageName()); - startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); + startAppFast(CALCULATOR_APP_PACKAGE); startTestActivity(2); } @@ -84,7 +85,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { executeOnLauncher(launcher -> assertTrue( "Launcher activity is the top activity; expecting another activity to be the top " + "one", - isInBackground(launcher))); + isInLaunchedApp(launcher))); } @Test @@ -102,7 +103,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public void testOverview() throws Exception { startTestAppsWithCheck(); // mLauncher.pressHome() also tests an important case of pressing home while in background. - Overview overview = mLauncher.pressHome().switchToOverview(); + Overview overview = mLauncher.goHome().switchToOverview(); assertTrue("Launcher internal state didn't switch to Overview", isInState(() -> LauncherState.OVERVIEW)); executeOnLauncher( @@ -127,7 +128,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward)); // Test opening a task. - OverviewTask task = mLauncher.pressHome().switchToOverview().getCurrentTask(); + OverviewTask task = mLauncher.goHome().switchToOverview().getCurrentTask(); assertNotNull("overview.getCurrentTask() returned null (1)", task); assertNotNull("OverviewTask.open returned null", task.open()); assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject( @@ -136,10 +137,10 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { executeOnLauncher(launcher -> assertTrue( "Launcher activity is the top activity; expecting another activity to be the top " + "one", - isInBackground(launcher))); + isInLaunchedApp(launcher))); // Test dismissing a task. - overview = mLauncher.pressHome().switchToOverview(); + overview = mLauncher.goHome().switchToOverview(); assertTrue("Launcher internal state didn't switch to Overview", isInState(() -> LauncherState.OVERVIEW)); final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher)); @@ -151,7 +152,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { numTasks - 1, getTaskCount(launcher))); // Test dismissing all tasks. - mLauncher.pressHome().switchToOverview().dismissAllTasks(); + mLauncher.goHome().switchToOverview().dismissAllTasks(); assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL)); executeOnLauncher( @@ -168,17 +169,46 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @ScreenRecord // b/195673272 public void testOverviewActions() throws Exception { // Experimenting for b/165029151: - final Overview overview = mLauncher.pressHome().switchToOverview(); + final Overview overview = mLauncher.goHome().switchToOverview(); if (overview.hasTasks()) overview.dismissAllTasks(); - mLauncher.pressHome(); + mLauncher.goHome(); // startTestAppsWithCheck(); OverviewActions actionsView = - mLauncher.pressHome().switchToOverview().getOverviewActions(); + mLauncher.goHome().switchToOverview().getOverviewActions(); actionsView.clickAndDismissScreenshot(); } + @Test + @PortraitLandscape + public void testSplitFromOverview() { + assumeTrue(!mLauncher.isTablet()); + + startTestActivity(2); + startTestActivity(3); + + mLauncher.goHome().switchToOverview().getCurrentTask() + .tapMenu() + .tapSplitMenuItem() + .getCurrentTask() + .open(); + } + + @Test + @PortraitLandscape + public void testSplitFromOverviewForTablet() { + assumeTrue(mLauncher.isTablet()); + + startTestActivity(2); + startTestActivity(3); + + mLauncher.goHome().switchToOverview().getOverviewActions() + .clickSplit() + .getTestActivityTask(2) + .open(); + } + private int getCurrentOverviewPage(Launcher launcher) { return launcher.getOverviewPanel().getCurrentPage(); } @@ -195,40 +225,57 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { return launcher.getOverviewPanel().getBottomRowTaskCountForTablet(); } + @Ignore @Test @NavigationModeSwitch @PortraitLandscape + @ScreenRecord // b/238461765 public void testSwitchToOverview() throws Exception { + startTestAppsWithCheck(); assertNotNull("Workspace.switchToOverview() returned null", - mLauncher.pressHome().switchToOverview()); + mLauncher.goHome().switchToOverview()); assertTrue("Launcher internal state didn't switch to Overview", isInState(() -> LauncherState.OVERVIEW)); } + @Ignore @Test @NavigationModeSwitch @PortraitLandscape public void testBackground() throws Exception { - startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - final Background background = getAndAssertBackground(); + startAppFast(CALCULATOR_APP_PACKAGE); + final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); - assertNotNull("Background.switchToOverview() returned null", background.switchToOverview()); + assertNotNull("Background.switchToOverview() returned null", + launchedAppState.switchToOverview()); assertTrue("Launcher internal state didn't switch to Overview", isInState(() -> LauncherState.OVERVIEW)); } - private Background getAndAssertBackground() { - final Background background = mLauncher.getBackground(); - assertNotNull("Launcher.getBackground() returned null", background); + private LaunchedAppState getAndAssertLaunchedApp() { + final LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState(); + assertNotNull("Launcher.getLaunchedApp() returned null", launchedAppState); executeOnLauncher(launcher -> assertTrue( "Launcher activity is the top activity; expecting another activity to be the top " + "one", - isInBackground(launcher))); - return background; + isInLaunchedApp(launcher))); + return launchedAppState; + } + + private void quickSwitchToPreviousAppAndAssert(boolean toRight) { + final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); + if (toRight) { + launchedAppState.quickSwitchToPreviousApp(); + } else { + launchedAppState.quickSwitchToPreviousAppSwipeLeft(); + } + + // While enable shell transition, Launcher can be resumed due to transient launch. + waitForLauncherCondition("Launcher shouldn't stay in resume forever", + this::isInLaunchedApp, 3000 /* timeout */); } @Test - @Ignore("b/197802324") @PortraitLandscape public void testAllAppsFromHome() throws Exception { // Test opening all apps @@ -240,7 +287,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { // Testing pressHome. assertTrue("Launcher internal state is not All Apps", isInState(() -> LauncherState.ALL_APPS)); - assertNotNull("pressHome returned null", mLauncher.pressHome()); + assertNotNull("pressHome returned null", mLauncher.goHome()); assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL)); assertNotNull("getHome returned null", mLauncher.getWorkspace()); @@ -254,13 +301,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestActivity(3); startTestActivity(4); - Background background = getAndAssertBackground(); - background.quickSwitchToPreviousApp(); + quickSwitchToPreviousAppAndAssert(true /* toRight */); assertTrue("The first app we should have quick switched to is not running", isTestActivityRunning(3)); - background = getAndAssertBackground(); - background.quickSwitchToPreviousApp(); + quickSwitchToPreviousAppAndAssert(true /* toRight */); if (mLauncher.getNavigationModel() == NavigationModel.THREE_BUTTON) { // 3-button mode toggles between 2 apps, rather than going back further. assertTrue("Second quick switch should have returned to the first app.", @@ -269,13 +314,37 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { assertTrue("The second app we should have quick switched to is not running", isTestActivityRunning(2)); } - background = getAndAssertBackground(); - background.quickSwitchToPreviousAppSwipeLeft(); + + quickSwitchToPreviousAppAndAssert(false /* toRight */); assertTrue("The 2nd app we should have quick switched to is not running", isTestActivityRunning(3)); - background = getAndAssertBackground(); - background.switchToOverview(); + final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); + launchedAppState.switchToOverview(); + } + + @Test + @ScreenRecord // b/242163205 + public void testQuickSwitchToPreviousAppForTablet() throws Exception { + assumeTrue(mLauncher.isTablet()); + startTestActivity(2); + startImeTestActivity(); + + // Set ignoreTaskbarVisibility to true to verify the task bar visibility explicitly. + mLauncher.setIgnoreTaskbarVisibility(true); + + // Expect task bar invisible when the launched app was the IME activity. + LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); + launchedAppState.assertTaskbarHidden(); + + // Quick-switch to the test app with swiping to right. + quickSwitchToPreviousAppAndAssert(true /* toRight */); + + assertTrue("The first app we should have quick switched to is not running", + isTestActivityRunning(2)); + // Expect task bar visible when the launched app was the test activity. + launchedAppState = getAndAssertLaunchedApp(); + launchedAppState.assertTaskbarVisible(); } private boolean isTestActivityRunning(int activityNumber) { @@ -289,49 +358,38 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @PortraitLandscape public void testQuickSwitchFromHome() throws Exception { startTestActivity(2); - mLauncher.pressHome().quickSwitchToPreviousApp(); + mLauncher.goHome().quickSwitchToPreviousApp(); assertTrue("The most recent task is not running after quick switching from home", isTestActivityRunning(2)); - getAndAssertBackground(); + getAndAssertLaunchedApp(); } - // TODO(b/204830798): test with all navigation modes(add @NavigationModeSwitch annotation) - // after the bug resolved. - @Ignore("b/205027405") @Test @PortraitLandscape - @ScreenRecord + @NavigationModeSwitch public void testPressBack() throws Exception { mLauncher.getWorkspace().switchToAllApps(); mLauncher.pressBack(); mLauncher.getWorkspace(); waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); - AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); - allApps.freeze(); - try { - allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); - } finally { - allApps.unfreeze(); - } - mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName()); + startAppFast(CALCULATOR_APP_PACKAGE); mLauncher.pressBack(); mLauncher.getWorkspace(); waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); } + @Ignore @Test @PortraitLandscape public void testOverviewForTablet() throws Exception { - // TODO(b/210158657): Re-enable for OOP - if (!mLauncher.isTablet() || !TestHelpers.isInLauncherProcess()) { - return; - } + assumeTrue(mLauncher.isTablet()); + for (int i = 2; i <= 14; i++) { startTestActivity(i); } - Overview overview = mLauncher.pressHome().switchToOverview(); + Overview overview = mLauncher.goHome().switchToOverview(); executeOnLauncher( launcher -> assertTrue("Don't have at least 13 tasks", getTaskCount(launcher) >= 13)); @@ -346,11 +404,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { // Test opening the task. overview.getCurrentTask().open(); assertTrue("Test activity didn't open from Overview", - mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity8")), + mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity10")), DEFAULT_UI_TIMEOUT)); // Scroll the task offscreen as it is now first - overview = mLauncher.pressHome().switchToOverview(); + overview = mLauncher.goHome().switchToOverview(); overview.scrollCurrentTaskOffScreen(); assertTrue("Launcher internal state is not Overview", isInState(() -> LauncherState.OVERVIEW)); @@ -379,7 +437,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { launcher)) <= 1))); // Test dismissing all tasks. - mLauncher.pressHome().switchToOverview().dismissAllTasks(); + mLauncher.goHome().switchToOverview().dismissAllTasks(); assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL)); executeOnLauncher( diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java new file mode 100644 index 0000000000..d3fbe93632 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java @@ -0,0 +1,82 @@ +/* + * 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; + +import android.content.Intent; + +import com.android.launcher3.ui.TaplTestsLauncher3; +import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +public class TaplTestsSplitscreen extends AbstractQuickStepTest { + private static final String CALCULATOR_APP_NAME = "Calculator"; + private static final String CALCULATOR_APP_PACKAGE = + resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + TaplTestsLauncher3.initialize(this); + + mLauncher.getWorkspace() + .deleteAppIcon(mLauncher.getWorkspace().getHotseatAppIcon(0)) + .switchToAllApps() + .getAppIcon(CALCULATOR_APP_NAME) + .dragToHotseat(0); + + startAppFast(CALCULATOR_APP_PACKAGE); + if (mLauncher.isTablet()) { + mLauncher.enableBlockTimeout(true); + mLauncher.showTaskbarIfHidden(); + } + } + + @After + public void tearDown() { + if (mLauncher.isTablet()) { + mLauncher.enableBlockTimeout(false); + } + } + + @Test + // TODO (b/270201357): When this test is proven stable, remove this TestStabilityRule and + // introduce into presubmit as well. + @TestStabilityRule.Stability( + flavors = TestStabilityRule.LOCAL | TestStabilityRule.PLATFORM_POSTSUBMIT) + @PortraitLandscape + @TaskbarModeSwitch + public void testSplitAppFromHomeWithItself() throws Exception { + Assume.assumeTrue(mLauncher.isTablet()); + + mLauncher.goHome() + .switchToAllApps() + .getAppIcon(CALCULATOR_APP_NAME) + .openMenu() + .getSplitScreenMenuItem() + .click(); + + mLauncher.getLaunchedAppState() + .getTaskbar() + .getAppIcon(CALCULATOR_APP_NAME) + .launchIntoSplitScreen(); + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java new file mode 100644 index 0000000000..735c5e6fd4 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT; + +import static junit.framework.TestCase.assertEquals; + +import android.content.Intent; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.tapl.Taskbar; +import com.android.launcher3.ui.TaplTestsLauncher3; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; +import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class TaplTestsTaskbar extends AbstractQuickStepTest { + + private static final String TEST_APP_NAME = "LauncherTestApp"; + private static final String TEST_APP_PACKAGE = + getInstrumentation().getContext().getPackageName(); + private static final String CALCULATOR_APP_PACKAGE = + resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); + + @Override + public void setUp() throws Exception { + Assume.assumeTrue(mLauncher.isTablet()); + super.setUp(); + mLauncher.useTestWorkspaceLayoutOnReload(); + TaplTestsLauncher3.initialize(this); + + startAppFast(CALCULATOR_APP_PACKAGE); + mLauncher.enableBlockTimeout(true); + mLauncher.showTaskbarIfHidden(); + } + + @After + public void tearDown() { + mLauncher.useDefaultWorkspaceLayoutOnReload(); + mLauncher.enableBlockTimeout(false); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testHideShowTaskbar() { + getTaskbar().hide(); + mLauncher.getLaunchedAppState().showTaskbar(); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testHideTaskbarPersistsOnRecreate() { + getTaskbar().hide(); + mLauncher.recreateTaskbar(); + mLauncher.getLaunchedAppState().assertTaskbarHidden(); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchApp() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchApp() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + mLauncher.getLaunchedAppState().assertTaskbarHidden(); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testOpenMenu() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).openMenu(); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientOpenMenu() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).openMenu(); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchShortcut() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .launch(TEST_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchShortcut() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .launch(TEST_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchAppInSplitscreen() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).dragToSplitscreen( + TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchAppInSplitscreen() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME).dragToSplitscreen( + TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + mLauncher.getLaunchedAppState().assertTaskbarHidden(); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchShortcutInSplitscreen() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchShortcutInSplitscreen() throws Exception { + getTaskbar().getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchApp_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchApp_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testOpenMenu_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).openMenu(); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientOpenMenu_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).openMenu(); + } + + @Test + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchShortcut_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .launch(TEST_APP_PACKAGE); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchShortcut_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .launch(TEST_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchAppInSplitscreen_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchAppInSplitscreen_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = PERSISTENT) + public void testLaunchShortcutInSplitscreen_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + @Test + @ScreenRecord // b/231615831 + @PortraitLandscape + @TaskbarModeSwitch(mode = TRANSIENT) + public void testTransientLaunchShortcutInSplitscreen_FromTaskbarAllApps() throws Exception { + getTaskbar().openAllApps() + .getAppIcon(TEST_APP_NAME) + .openDeepShortcutMenu() + .getMenuItem("Shortcut 1") + .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE); + } + + private Taskbar getTaskbar() { + Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar(); + List taskbarIconNames = taskbar.getIconNames(); + List hotseatIconNames = mLauncher.getHotseatIconNames(); + + assertEquals("Taskbar and hotseat icon counts do not match", + taskbarIconNames.size(), hotseatIconNames.size()); + + for (int i = 0; i < taskbarIconNames.size(); i++) { + assertEquals("Taskbar and Hotseat icons do not match", + taskbarIconNames, hotseatIconNames); + } + + return taskbar; + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java new file mode 100644 index 0000000000..9e41f74186 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.ALL; +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT; + +import android.content.Context; +import android.util.Log; + +import com.android.launcher3.tapl.LauncherInstrumentation; +import com.android.launcher3.tapl.TestHelpers; +import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.rule.FailureWatcher; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test rule that allows executing a test multiple times with different conditions + * ie. with transient taskbar enabled and disabled. + * The test should be annotated with @TaskbarModeSwitch. + */ +public class TaskbarModeSwitchRule implements TestRule { + + static final String TAG = "TaskbarModeSwitchRule"; + + public static final int WAIT_TIME_MS = 10000; + + public enum Mode { + TRANSIENT, PERSISTENT, ALL + } + + // Annotation for tests that need to be run with quickstep enabled and disabled. + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface TaskbarModeSwitch { + Mode mode() default ALL; + } + + private final LauncherInstrumentation mLauncher; + + public TaskbarModeSwitchRule(LauncherInstrumentation launcher) { + mLauncher = launcher; + } + + @Override + public Statement apply(Statement base, Description description) { + if (TestHelpers.isInLauncherProcess() + && description.getAnnotation(TaskbarModeSwitch.class) != null) { + Mode mode = description.getAnnotation(TaskbarModeSwitch.class).mode(); + return new Statement() { + @Override + public void evaluate() throws Throwable { + mLauncher.enableDebugTracing(); + final boolean wasTransientTaskbarMode = + isTaskbarTransientMode(getInstrumentation().getTargetContext()); + try { + if (mode == TRANSIENT || mode == ALL) { + evaluateWithTransientTaskbar(); + } + if (mode == PERSISTENT || mode == ALL) { + evaluateWithPersistentTaskbar(); + } + } catch (Throwable e) { + Log.e(TAG, "Error", e); + throw e; + } finally { + Log.d(TAG, "In Finally block"); + setTaskbarMode(mLauncher, wasTransientTaskbarMode, description); + } + } + + private void evaluateWithPersistentTaskbar() throws Throwable { + setTaskbarMode(mLauncher, false, description); + base.evaluate(); + } + + private void evaluateWithTransientTaskbar() throws Throwable { + setTaskbarMode(mLauncher, true, description); + base.evaluate(); + } + }; + } else { + return base; + } + } + + private static boolean isTaskbarTransientMode(Context context) { + return DisplayController.isTransientTaskbar(context); + } + + public static void setTaskbarMode(LauncherInstrumentation launcher, + boolean expectTransientTaskbar, Description description) throws Exception { + launcher.enableTransientTaskbar(expectTransientTaskbar); + launcher.recreateTaskbar(); + + Context context = getInstrumentation().getTargetContext(); + assertTrue(launcher, "Couldn't set taskbar=" + expectTransientTaskbar, + isTaskbarTransientMode(context) == expectTransientTaskbar, description); + + AbstractLauncherUiTest.checkDetectedLeaks(launcher); + } + + private static void assertTrue(LauncherInstrumentation launcher, String message, + boolean condition, Description description) { + launcher.checkForAnomaly(true, true); + if (!condition) { + final AssertionError assertionError = new AssertionError(message); + if (description != null) { + FailureWatcher.onError(launcher, description, assertionError); + } + throw assertionError; + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java index 0f5a1c8589..7e408a8587 100644 --- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java +++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java @@ -51,7 +51,7 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.data.LauncherAppWidgetInfo; -import com.android.launcher3.tapl.Background; +import com.android.launcher3.tapl.LaunchedAppState; import com.android.launcher3.testcomponent.ListViewService; import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory; import com.android.launcher3.testcomponent.TestCommandReceiver; @@ -119,13 +119,13 @@ public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest { try { // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - mLauncher.getBackground().switchToOverview().dismissAllTasks(); + mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); - mLauncher.getBackground().switchToOverview(); + mLauncher.getLaunchedAppState().switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); } finally { @@ -197,7 +197,7 @@ public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest { addItemToScreen(item); assertTrue("Widget is not present", - mLauncher.pressHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null); + mLauncher.goHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null); int widgetId = item.appWidgetId; // Verify widget id @@ -205,23 +205,23 @@ public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest { // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - mLauncher.getBackground().switchToOverview().dismissAllTasks(); + mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); - Background background = mLauncher.getBackground(); + LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState(); // Update widget updateBeforeSwipeUp.accept(widgetId); - background.switchToOverview(); + launchedAppState.switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); // Widget is updated when going home mInitTracker.disableLog(); - mLauncher.pressHome(); + mLauncher.goHome(); verifyWidget(finalWidgetText); assertNotEquals(1, mInitTracker.viewInitCount); } finally { diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt new file mode 100644 index 0000000000..7e07b813a7 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -0,0 +1,165 @@ +/* + * 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 android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.views.GroupedTaskView +import com.android.quickstep.views.IconView +import com.android.quickstep.views.TaskThumbnailView +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer +import com.android.systemui.shared.recents.model.Task +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.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class SplitAnimationControllerTest { + + private val taskId = 9 + + @Mock lateinit var mockSplitSelectStateController: SplitSelectStateController + // TaskView + @Mock lateinit var mockTaskView: TaskView + @Mock lateinit var mockThumbnailView: TaskThumbnailView + @Mock lateinit var mockBitmap: Bitmap + @Mock lateinit var mockIconView: IconView + @Mock lateinit var mockTaskViewDrawable: Drawable + // GroupedTaskView + @Mock lateinit var mockGroupedTaskView: GroupedTaskView + @Mock lateinit var mockTask: Task + @Mock lateinit var mockTaskKey: Task.TaskKey + @Mock lateinit var mockTaskIdAttributeContainer: TaskIdAttributeContainer + + // SplitSelectSource + @Mock lateinit var splitSelectSource: SplitConfigurationOptions.SplitSelectSource + @Mock lateinit var mockSplitSourceDrawable: Drawable + @Mock lateinit var mockSplitSourceView: View + + lateinit var splitAnimationController: SplitAnimationController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(mockTaskView.thumbnail).thenReturn(mockThumbnailView) + whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskView.iconView).thenReturn(mockIconView) + whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable) + + whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable) + whenever(splitSelectSource.view).thenReturn(mockSplitSourceView) + + splitAnimationController = SplitAnimationController(mockSplitSelectStateController) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewIcon_useSplitSourceIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + // Missing taskView icon + whenever(mockIconView.drawable).thenReturn(null) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not fallback to use splitSource icon drawable", + mockSplitSourceDrawable, splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_validTaskViewIcon_useTaskViewIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use taskView icon drawable", mockTaskViewDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_validTaskViewNullSplitSource_useTaskViewIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + // Set split source to null + whenever(splitSelectSource.drawable).thenReturn(null) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use taskView icon drawable", mockTaskViewDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewValidSplitSource_noTaskDismissal() { + // Hit initiating split from home + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(false) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use splitSource icon drawable", mockSplitSourceDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewValidSplitSource_groupedTaskView() { + // Hit groupedTaskView dismissal + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(true) + + // Remove icon view from GroupedTaskView + whenever(mockIconView.drawable).thenReturn(null) + + whenever(mockTaskIdAttributeContainer.task).thenReturn(mockTask) + whenever(mockTaskIdAttributeContainer.iconView).thenReturn(mockIconView) + whenever(mockTaskIdAttributeContainer.thumbnailView).thenReturn(mockThumbnailView) + whenever(mockTask.getKey()).thenReturn(mockTaskKey) + whenever(mockTaskKey.getId()).thenReturn(taskId) + whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId) + whenever(mockGroupedTaskView.taskIdAttributeContainers) + .thenReturn(Array(1) { mockTaskIdAttributeContainer }) + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockGroupedTaskView }, { splitSelectSource }) + + assertEquals("Did not use splitSource icon drawable", mockSplitSourceDrawable, + splitAnimInitProps.iconDrawable) + } +} \ No newline at end of file diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt new file mode 100644 index 0000000000..512df8e3d0 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -0,0 +1,405 @@ +/* + * 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 android.app.ActivityManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.os.Handler +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.LauncherState +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.statehandlers.DepthController +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.withArgCaptor +import com.android.quickstep.RecentsModel +import com.android.quickstep.SystemUiProxy +import com.android.systemui.shared.recents.model.Task +import java.util.ArrayList +import java.util.function.Consumer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class SplitSelectStateControllerTest { + + @Mock lateinit var systemUiProxy: SystemUiProxy + @Mock lateinit var depthController: DepthController + @Mock lateinit var statsLogManager: StatsLogManager + @Mock lateinit var stateManager: StateManager + @Mock lateinit var handler: Handler + @Mock lateinit var context: Context + @Mock lateinit var recentsModel: RecentsModel + + lateinit var splitSelectStateController: SplitSelectStateController + + private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId) + private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + splitSelectStateController = + SplitSelectStateController( + context, + handler, + stateManager, + depthController, + statsLogManager, + systemUiProxy, + recentsModel + ) + } + + @Test + fun activeTasks_noMatchingTasks() { + val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName("pomegranate", "juice"), + ComponentName("pumpkin", "pie") + ) + val groupTask2 = + generateGroupTask( + ComponentName("hotdog", "juice"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { assertNull("No tasks should have matched", it /*task*/) } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + nonMatchingComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_singleMatchingTask() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val matchingComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + ComponentName("pomegranate", "juice") + ) + val groupTask2 = + generateGroupTask( + ComponentName("pumpkin", "pie"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { + assertEquals( + "ComponentName package mismatched", + it.key.baseIntent.component.packageName, + matchingPackage + ) + assertEquals( + "ComponentName class mismatched", + it.key.baseIntent.component.className, + matchingClass + ) + assertEquals(it, groupTask1.task1) + } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + matchingComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_skipTaskWithDifferentUser() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val nonPrimaryUserComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + ComponentName("pomegranate", "juice") + ) + val groupTask2 = + generateGroupTask( + ComponentName("pumpkin", "pie"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { assertNull("No tasks should have matched", it /*task*/) } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + nonPrimaryUserComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_findTaskAsNonPrimaryUser() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val nonPrimaryUserComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + nonPrimaryUserHandle, + ComponentName("pomegranate", "juice"), + nonPrimaryUserHandle + ) + val groupTask2 = + generateGroupTask( + ComponentName("pumpkin", "pie"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { + assertEquals( + "ComponentName package mismatched", + it.key.baseIntent.component.packageName, + matchingPackage + ) + assertEquals( + "ComponentName class mismatched", + it.key.baseIntent.component.className, + matchingClass + ) + assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier) + assertEquals(it, groupTask1.task1) + } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + nonPrimaryUserComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_multipleMatchMostRecentTask() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val matchingComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + ComponentName("pumpkin", "pie") + ) + val groupTask2 = + generateGroupTask( + ComponentName("pomegranate", "juice"), + ComponentName(matchingPackage, matchingClass) + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask2) + tasks.add(groupTask1) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { + assertEquals( + "ComponentName package mismatched", + it.key.baseIntent.component.packageName, + matchingPackage + ) + assertEquals( + "ComponentName class mismatched", + it.key.baseIntent.component.className, + matchingClass + ) + assertEquals(it, groupTask2.task2) + } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + matchingComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun setInitialApp_withTaskId() { + splitSelectStateController.setInitialTaskSelect( + null /*intent*/, + -1 /*stagePosition*/, + ItemInfo(), + null /*splitEvent*/, + 10 /*alreadyRunningTask*/ + ) + assertTrue(splitSelectStateController.isSplitSelectActive) + } + + @Test + fun setInitialApp_withIntent() { + splitSelectStateController.setInitialTaskSelect( + Intent() /*intent*/, + -1 /*stagePosition*/, + ItemInfo(), + null /*splitEvent*/, + -1 /*alreadyRunningTask*/ + ) + assertTrue(splitSelectStateController.isSplitSelectActive) + } + + @Test + fun resetAfterInitial() { + splitSelectStateController.setInitialTaskSelect( + Intent() /*intent*/, + -1 /*stagePosition*/, + ItemInfo(), + null /*splitEvent*/, + -1 + ) + splitSelectStateController.resetState() + assertFalse(splitSelectStateController.isSplitSelectActive) + } + + // Generate GroupTask with default userId. + private fun generateGroupTask( + task1ComponentName: ComponentName, + task2ComponentName: ComponentName + ): GroupTask { + val task1 = Task() + var taskInfo = ActivityManager.RunningTaskInfo() + var intent = Intent() + intent.component = task1ComponentName + taskInfo.baseIntent = intent + task1.key = Task.TaskKey(taskInfo) + + val task2 = Task() + taskInfo = ActivityManager.RunningTaskInfo() + intent = Intent() + intent.component = task2ComponentName + taskInfo.baseIntent = intent + task2.key = Task.TaskKey(taskInfo) + return GroupTask( + task1, + task2, + SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1) + ) + } + + // Generate GroupTask with custom user handles. + private fun generateGroupTask( + task1ComponentName: ComponentName, + userHandle1: UserHandle, + task2ComponentName: ComponentName, + userHandle2: UserHandle + ): GroupTask { + val task1 = Task() + var taskInfo = ActivityManager.RunningTaskInfo() + // Apply custom userHandle1 + taskInfo.userId = userHandle1.identifier + var intent = Intent() + intent.component = task1ComponentName + taskInfo.baseIntent = intent + task1.key = Task.TaskKey(taskInfo) + val task2 = Task() + taskInfo = ActivityManager.RunningTaskInfo() + // Apply custom userHandle2 + taskInfo.userId = userHandle2.identifier + intent = Intent() + intent.component = task2ComponentName + taskInfo.baseIntent = intent + task2.key = Task.TaskKey(taskInfo) + return GroupTask( + task1, + task2, + SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1) + ) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java index 9c5cfcd594..83602be72e 100644 --- a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java +++ b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java @@ -15,19 +15,16 @@ */ package com.android.quickstep.util; -import static android.view.Display.DEFAULT_DISPLAY; - import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Display; +import android.util.ArrayMap; +import android.view.RemoteAnimationTarget; import android.view.Surface; -import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -37,12 +34,15 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.LauncherModelHelper; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.ReflectionHelpers; +import com.android.launcher3.util.RotationUtils; +import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.window.CachedDisplayInfo; +import com.android.launcher3.util.window.WindowManagerProxy; import com.android.quickstep.FallbackActivityInterface; -import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.SystemUiProxy; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; +import com.android.quickstep.util.SurfaceTransaction.MockProperties; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; @@ -143,30 +143,35 @@ public class TaskViewSimulatorTest { LauncherModelHelper helper = new LauncherModelHelper(); try { helper.sandboxContext.allow(SystemUiProxy.INSTANCE); - helper.sandboxContext.allow(SysUINavigationMode.INSTANCE); + int rotation = mDisplaySize.x > mDisplaySize.y + ? Surface.ROTATION_90 : Surface.ROTATION_0; + CachedDisplayInfo cdi = + new CachedDisplayInfo(mDisplaySize, rotation, new Rect()); + WindowBounds wm = new WindowBounds( + new Rect(0, 0, mDisplaySize.x, mDisplaySize.y), + mDisplayInsets); + WindowBounds[] allBounds = new WindowBounds[4]; + for (int i = 0; i < 4; i++) { + Rect boundsR = new Rect(wm.bounds); + Rect insetsR = new Rect(wm.insets); - Display display = mock(Display.class); - doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); - doReturn(mDisplaySize.x > mDisplaySize.y ? Surface.ROTATION_90 : Surface.ROTATION_0) - .when(display).getRotation(); - doAnswer(i -> { - ((Point) i.getArgument(0)).set(mDisplaySize.x, mDisplaySize.y); - return null; - }).when(display).getRealSize(any()); - doAnswer(i -> { - Point smallestSize = i.getArgument(0); - Point largestSize = i.getArgument(1); - smallestSize.x = smallestSize.y = Math.min(mDisplaySize.x, mDisplaySize.y); - largestSize.x = largestSize.y = Math.max(mDisplaySize.x, mDisplaySize.y); + RotationUtils.rotateRect(insetsR, RotationUtils.deltaRotation(rotation, i)); + RotationUtils.rotateRect(boundsR, RotationUtils.deltaRotation(rotation, i)); + boundsR.set(0, 0, Math.abs(boundsR.width()), Math.abs(boundsR.height())); + allBounds[i] = new WindowBounds(boundsR, insetsR); + } - smallestSize.x -= mDisplayInsets.left + mDisplayInsets.right; - largestSize.x -= mDisplayInsets.left + mDisplayInsets.right; + WindowManagerProxy wmProxy = mock(WindowManagerProxy.class); + doReturn(cdi).when(wmProxy).getDisplayInfo(any()); + doReturn(wm).when(wmProxy).getRealBounds(any(), any()); + doReturn(NavigationMode.NO_BUTTON).when(wmProxy).getNavigationMode(any()); - smallestSize.y -= mDisplayInsets.top + mDisplayInsets.bottom; - largestSize.y -= mDisplayInsets.top + mDisplayInsets.bottom; - return null; - }).when(display).getCurrentSizeRange(any(), any()); - DisplayController.Info mockInfo = new Info(helper.sandboxContext, display); + ArrayMap perDisplayBoundsCache = + new ArrayMap<>(); + perDisplayBoundsCache.put(cdi.normalize(), allBounds); + + DisplayController.Info mockInfo = new Info( + helper.sandboxContext, wmProxy, perDisplayBoundsCache); DisplayController controller = DisplayController.INSTANCE.get(helper.sandboxContext); @@ -174,7 +179,7 @@ public class TaskViewSimulatorTest { ReflectionHelpers.setField(controller, "mInfo", mockInfo); mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(helper.sandboxContext) - .getBestMatch(mAppBounds.width(), mAppBounds.height()); + .getBestMatch(mAppBounds.width(), mAppBounds.height(), rotation); mDeviceProfile.updateInsets(mLauncherInsets); TaskViewSimulator tvs = new TaskViewSimulator(helper.sandboxContext, @@ -201,17 +206,21 @@ public class TaskViewSimulatorTest { } @Override - public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) { - SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null); - proxy.onBuildTargetParams(builder, mock(RemoteAnimationTargetCompat.class), this); - return new SurfaceParams[] {builder.build()}; + public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) { + RecordingSurfaceTransaction transaction = new RecordingSurfaceTransaction(); + proxy.onBuildTargetParams( + transaction.mockProperties, mock(RemoteAnimationTarget.class), this); + return transaction; } @Override - public void applySurfaceParams(SurfaceParams[] params) { + public void applySurfaceParams(SurfaceTransaction params) { + Assert.assertTrue(params instanceof RecordingSurfaceTransaction); + MockProperties p = ((RecordingSurfaceTransaction) params).mockProperties; + // Verify that the task position remains the same RectF newAppBounds = new RectF(mAppBounds); - params[0].matrix.mapRect(newAppBounds); + p.matrix.mapRect(newAppBounds); Assert.assertThat(newAppBounds, new AlmostSame(mAppBounds)); System.err.println("Bounds mapped: " + mAppBounds + " => " + newAppBounds); diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml new file mode 100644 index 0000000000..94ef06c604 --- /dev/null +++ b/res/anim-v33/shared_x_axis_activity_close_enter.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ 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 new file mode 100644 index 0000000000..19eb09e4d3 --- /dev/null +++ b/res/anim-v33/shared_x_axis_activity_close_exit.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ 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 new file mode 100644 index 0000000000..f699ceca70 --- /dev/null +++ b/res/anim-v33/shared_x_axis_activity_open_enter.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ 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 new file mode 100644 index 0000000000..85988ecfd2 --- /dev/null +++ b/res/anim-v33/shared_x_axis_activity_open_exit.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/color-night-v31/all_apps_button_color_2.xml b/res/color-night-v31/all_apps_button_color_2.xml new file mode 100644 index 0000000000..e4005439ff --- /dev/null +++ b/res/color-night-v31/all_apps_button_color_2.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-night-v31/all_apps_tab_text.xml b/res/color-night-v31/all_apps_tab_text.xml index 83237b49e5..54b95aee8d 100644 --- a/res/color-night-v31/all_apps_tab_text.xml +++ b/res/color-night-v31/all_apps_tab_text.xml @@ -14,6 +14,6 @@ limitations under the License. --> - - + + \ No newline at end of file diff --git a/res/color-night-v31/all_apps_tabs_background.xml b/res/color-night-v31/all_apps_tabs_background.xml new file mode 100644 index 0000000000..9213274d5c --- /dev/null +++ b/res/color-night-v31/all_apps_tabs_background.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/color-night-v31/folder_background_dark.xml b/res/color-night-v31/folder_background_dark.xml index d607395155..696e8ea67e 100644 --- a/res/color-night-v31/folder_background_dark.xml +++ b/res/color-night-v31/folder_background_dark.xml @@ -15,6 +15,6 @@ --> diff --git a/res/color-night-v31/folder_preview_dark.xml b/res/color-night-v31/folder_preview_dark.xml index a5bd6367d5..bdd48a24bd 100644 --- a/res/color-night-v31/folder_preview_dark.xml +++ b/res/color-night-v31/folder_preview_dark.xml @@ -15,6 +15,6 @@ --> diff --git a/res/color-night-v31/taskbar_background.xml b/res/color-night-v31/taskbar_background.xml new file mode 100644 index 0000000000..3409d52b25 --- /dev/null +++ b/res/color-night-v31/taskbar_background.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/res/color-v31/all_apps_button_bg_color.xml b/res/color-v31/all_apps_button_bg_color.xml new file mode 100644 index 0000000000..89590207d5 --- /dev/null +++ b/res/color-v31/all_apps_button_bg_color.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-v31/all_apps_button_color_1.xml b/res/color-v31/all_apps_button_color_1.xml new file mode 100644 index 0000000000..71c7d8dc49 --- /dev/null +++ b/res/color-v31/all_apps_button_color_1.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-v31/all_apps_button_color_2.xml b/res/color-v31/all_apps_button_color_2.xml new file mode 100644 index 0000000000..608c8a929b --- /dev/null +++ b/res/color-v31/all_apps_button_color_2.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-v31/all_apps_button_color_3.xml b/res/color-v31/all_apps_button_color_3.xml new file mode 100644 index 0000000000..dbb97b13b2 --- /dev/null +++ b/res/color-v31/all_apps_button_color_3.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-v31/all_apps_button_color_4.xml b/res/color-v31/all_apps_button_color_4.xml new file mode 100644 index 0000000000..d02528f638 --- /dev/null +++ b/res/color-v31/all_apps_button_color_4.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/color-v31/folder_background_light.xml b/res/color-v31/folder_background_light.xml index e3c7e7dab1..eb2fdd7367 100644 --- a/res/color-v31/folder_background_light.xml +++ b/res/color-v31/folder_background_light.xml @@ -15,6 +15,6 @@ --> diff --git a/res/color-v31/folder_preview_light.xml b/res/color-v31/folder_preview_light.xml index fe30c87202..ed1205ebb9 100644 --- a/res/color-v31/folder_preview_light.xml +++ b/res/color-v31/folder_preview_light.xml @@ -15,6 +15,6 @@ --> diff --git a/res/color-v31/overview_scrim.xml b/res/color-v31/overview_scrim.xml index 80799957ff..212518ff65 100644 --- a/res/color-v31/overview_scrim.xml +++ b/res/color-v31/overview_scrim.xml @@ -14,5 +14,5 @@ limitations under the License. --> - + diff --git a/res/color-v31/taskbar_background.xml b/res/color-v31/taskbar_background.xml new file mode 100644 index 0000000000..5c53e4a0c9 --- /dev/null +++ b/res/color-v31/taskbar_background.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/res/color/app_subtitle_text_dark.xml b/res/color/app_subtitle_text_dark.xml new file mode 100644 index 0000000000..220d10fc8f --- /dev/null +++ b/res/color/app_subtitle_text_dark.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/res/color/app_subtitle_text_light.xml b/res/color/app_subtitle_text_light.xml new file mode 100644 index 0000000000..fb00baac6c --- /dev/null +++ b/res/color/app_subtitle_text_light.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/res/color/app_title_text_dark.xml b/res/color/app_title_text_dark.xml new file mode 100644 index 0000000000..220d10fc8f --- /dev/null +++ b/res/color/app_title_text_dark.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/res/color/app_title_text_light.xml b/res/color/app_title_text_light.xml new file mode 100644 index 0000000000..bb5297319d --- /dev/null +++ b/res/color/app_title_text_light.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/res/color/system_shortcut_text.xml b/res/color/system_shortcut_text.xml new file mode 100644 index 0000000000..f9f8239a99 --- /dev/null +++ b/res/color/system_shortcut_text.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/res/color/widgets_picker_scrim.xml b/res/color/widgets_picker_scrim.xml index 1cf97f61f7..5d5130086a 100644 --- a/res/color/widgets_picker_scrim.xml +++ b/res/color/widgets_picker_scrim.xml @@ -18,5 +18,5 @@ */ --> - + diff --git a/res/drawable-sw600dp/ic_transient_taskbar_all_apps_button.xml b/res/drawable-sw600dp/ic_transient_taskbar_all_apps_button.xml new file mode 100644 index 0000000000..6e740aed4f --- /dev/null +++ b/res/drawable-sw600dp/ic_transient_taskbar_all_apps_button.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml new file mode 100644 index 0000000000..47f2a5d73a --- /dev/null +++ b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/res/drawable/bg_rounded_corner_bottom_sheet.xml b/res/drawable/bg_rounded_corner_bottom_sheet.xml index aa49bced7a..dfcd354ce7 100644 --- a/res/drawable/bg_rounded_corner_bottom_sheet.xml +++ b/res/drawable/bg_rounded_corner_bottom_sheet.xml @@ -16,7 +16,7 @@ - + diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml new file mode 100644 index 0000000000..c92956e005 --- /dev/null +++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/res/drawable/bg_widgets_content.xml b/res/drawable/bg_widgets_content.xml new file mode 100644 index 0000000000..b0b699ba22 --- /dev/null +++ b/res/drawable/bg_widgets_content.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/bg_widgets_header.xml b/res/drawable/bg_widgets_header.xml new file mode 100644 index 0000000000..a89aad474b --- /dev/null +++ b/res/drawable/bg_widgets_header.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_large_screen.xml b/res/drawable/bg_widgets_header_large_screen.xml new file mode 100644 index 0000000000..e1408cc235 --- /dev/null +++ b/res/drawable/bg_widgets_header_large_screen.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/res/drawable/bg_widgets_header_states.xml b/res/drawable/bg_widgets_header_states.xml new file mode 100644 index 0000000000..f45a7ababf --- /dev/null +++ b/res/drawable/bg_widgets_header_states.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/bg_widgets_header_states_large_screen.xml b/res/drawable/bg_widgets_header_states_large_screen.xml new file mode 100644 index 0000000000..1ee5fe5585 --- /dev/null +++ b/res/drawable/bg_widgets_header_states_large_screen.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/bg_work_apps_paused_action_button.xml b/res/drawable/bg_work_apps_paused_action_button.xml new file mode 100644 index 0000000000..57d1ae5938 --- /dev/null +++ b/res/drawable/bg_work_apps_paused_action_button.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/drop_target_frame.xml b/res/drawable/drop_target_frame.xml index 666a96e940..9f04103510 100644 --- a/res/drawable/drop_target_frame.xml +++ b/res/drawable/drop_target_frame.xml @@ -17,6 +17,6 @@ - + \ No newline at end of file diff --git a/res/drawable/gm_edit_24.xml b/res/drawable/gm_edit_24.xml index 59a0dc2faf..f7413334f9 100644 --- a/res/drawable/gm_edit_24.xml +++ b/res/drawable/gm_edit_24.xml @@ -5,6 +5,6 @@ android:viewportHeight="24" android:tint="?attr/colorControlNormal"> diff --git a/res/drawable/ic_all_apps_bg_hand.xml b/res/drawable/ic_all_apps_bg_hand.xml deleted file mode 100644 index b980e9e6f2..0000000000 --- a/res/drawable/ic_all_apps_bg_hand.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/drawable/ic_all_apps_bg_icon_1.xml b/res/drawable/ic_all_apps_bg_icon_1.xml deleted file mode 100644 index d226ac6000..0000000000 --- a/res/drawable/ic_all_apps_bg_icon_1.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/ic_all_apps_bg_icon_2.xml b/res/drawable/ic_all_apps_bg_icon_2.xml deleted file mode 100644 index 5966d9969f..0000000000 --- a/res/drawable/ic_all_apps_bg_icon_2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/ic_all_apps_bg_icon_3.xml b/res/drawable/ic_all_apps_bg_icon_3.xml deleted file mode 100644 index b18f8bc473..0000000000 --- a/res/drawable/ic_all_apps_bg_icon_3.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - diff --git a/res/drawable/ic_all_apps_bg_icon_4.xml b/res/drawable/ic_all_apps_bg_icon_4.xml deleted file mode 100644 index 8eb4d90126..0000000000 --- a/res/drawable/ic_all_apps_bg_icon_4.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/res/drawable/ic_caption_desktop_button_foreground.xml b/res/drawable/ic_caption_desktop_button_foreground.xml new file mode 100644 index 0000000000..f185436dcf --- /dev/null +++ b/res/drawable/ic_caption_desktop_button_foreground.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/ic_select_windows.xml b/res/drawable/ic_select_windows.xml new file mode 100644 index 0000000000..cba0fde614 --- /dev/null +++ b/res/drawable/ic_select_windows.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_split_horizontal.xml b/res/drawable/ic_split_horizontal.xml index ee710d0797..2efd2b9bc2 100644 --- a/res/drawable/ic_split_horizontal.xml +++ b/res/drawable/ic_split_horizontal.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/res/drawable/ic_split_left.xml b/res/drawable/ic_split_left.xml deleted file mode 100644 index fc9f699c29..0000000000 --- a/res/drawable/ic_split_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/res/drawable/ic_split_right.xml b/res/drawable/ic_split_right.xml deleted file mode 100644 index cc156225e8..0000000000 --- a/res/drawable/ic_split_right.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/res/drawable/ic_split_top.xml b/res/drawable/ic_split_top.xml deleted file mode 100644 index f8c15bd44d..0000000000 --- a/res/drawable/ic_split_top.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/res/drawable/ic_taskbar_all_apps_button.xml b/res/drawable/ic_taskbar_all_apps_button.xml new file mode 100644 index 0000000000..82fbbea617 --- /dev/null +++ b/res/drawable/ic_taskbar_all_apps_button.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/res/drawable/page_indicator.xml b/res/drawable/page_indicator.xml new file mode 100644 index 0000000000..c0ccc49250 --- /dev/null +++ b/res/drawable/page_indicator.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/popup_background_material_u.xml b/res/drawable/popup_background_material_u.xml new file mode 100644 index 0000000000..4d40ba8afd --- /dev/null +++ b/res/drawable/popup_background_material_u.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml index b9942c0c92..9deab6eb57 100644 --- a/res/drawable/rounded_action_button.xml +++ b/res/drawable/rounded_action_button.xml @@ -18,10 +18,11 @@ + + android:color="?attr/colorSurfaceVariant" /> diff --git a/res/drawable/widget_suggestions.xml b/res/drawable/widget_suggestions.xml new file mode 100644 index 0000000000..b090a68a43 --- /dev/null +++ b/res/drawable/widget_suggestions.xml @@ -0,0 +1,27 @@ + + + + diff --git a/res/drawable/widget_suggestions_icon.xml b/res/drawable/widget_suggestions_icon.xml new file mode 100644 index 0000000000..919b5e4452 --- /dev/null +++ b/res/drawable/widget_suggestions_icon.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/res/drawable/widgets_recommendation_background.xml b/res/drawable/widgets_surface_background.xml similarity index 100% rename from res/drawable/widgets_recommendation_background.xml rename to res/drawable/widgets_surface_background.xml diff --git a/res/drawable/widgets_tray_expand_button.xml b/res/drawable/widgets_tray_expand_button.xml index 8316e0fbe3..f2e142ea24 100644 --- a/res/drawable/widgets_tray_expand_button.xml +++ b/res/drawable/widgets_tray_expand_button.xml @@ -14,8 +14,8 @@ limitations under the License. --> - - diff --git a/res/drawable/work_apps_toggle_background.xml b/res/drawable/work_apps_toggle_background.xml index a47c8fef7c..6ad6c821db 100644 --- a/res/drawable/work_apps_toggle_background.xml +++ b/res/drawable/work_apps_toggle_background.xml @@ -13,16 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - - + @@ -32,4 +24,4 @@ android:right="@dimen/work_profile_footer_padding" /> - + diff --git a/res/color-night-v31/widgets_picker_scrim.xml b/res/interpolator/back_cancel.xml similarity index 72% rename from res/color-night-v31/widgets_picker_scrim.xml rename to res/interpolator/back_cancel.xml index be7010b80b..2165457c72 100644 --- a/res/color-night-v31/widgets_picker_scrim.xml +++ b/res/interpolator/back_cancel.xml @@ -1,8 +1,7 @@ - - - + + \ No newline at end of file diff --git a/res/interpolator/fast_out_extra_slow_in.xml b/res/interpolator/fast_out_extra_slow_in.xml new file mode 100644 index 0000000000..f296a8224f --- /dev/null +++ b/res/interpolator/fast_out_extra_slow_in.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/res/interpolator/standard_accelerate.xml b/res/interpolator/standard_accelerate.xml new file mode 100644 index 0000000000..394393dc36 --- /dev/null +++ b/res/interpolator/standard_accelerate.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/res/interpolator/standard_decelerate.xml b/res/interpolator/standard_decelerate.xml new file mode 100644 index 0000000000..579f4f5644 --- /dev/null +++ b/res/interpolator/standard_decelerate.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml index 7f9f63ea18..655c75d1b5 100644 --- a/res/layout/all_apps.xml +++ b/res/layout/all_apps.xml @@ -17,35 +17,9 @@ will bake the left/right padding into that view's background itself. --> - - - - - - - - - - - - - - - \ No newline at end of file + android:saveEnabled="false" /> \ No newline at end of file diff --git a/res/layout/all_apps_bottom_sheet_background.xml b/res/layout/all_apps_bottom_sheet_background.xml new file mode 100644 index 0000000000..b0157c9c3e --- /dev/null +++ b/res/layout/all_apps_bottom_sheet_background.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/res/layout/all_apps_content.xml b/res/layout/all_apps_content.xml new file mode 100644 index 0000000000..925f4d963b --- /dev/null +++ b/res/layout/all_apps_content.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/all_apps_fast_scroller.xml b/res/layout/all_apps_fast_scroller.xml index 5537bc60a5..0f1d9330a6 100644 --- a/res/layout/all_apps_fast_scroller.xml +++ b/res/layout/all_apps_fast_scroller.xml @@ -21,7 +21,8 @@ android:id="@+id/fast_scroller_popup" style="@style/FastScrollerPopup" android:layout_alignParentEnd="true" - android:layout_below="@+id/search_container_all_apps" + android:layout_alignTop="@+id/all_apps_header" + android:layout_marginTop="@dimen/all_apps_header_bottom_padding" android:layout_marginEnd="@dimen/fastscroll_popup_margin" /> diff --git a/res/layout/all_apps_icon_twoline.xml b/res/layout/all_apps_icon_twoline.xml index 54c714734c..b0d02c15de 100644 --- a/res/layout/all_apps_icon_twoline.xml +++ b/res/layout/all_apps_icon_twoline.xml @@ -19,7 +19,6 @@ android:id="@+id/icon" android:singleLine="false" android:lines="2" - android:inputType="textMultiLine" launcher:iconDisplay="all_apps" launcher:centerVertically="true" /> diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml index 11143fb3e1..e04b207a3e 100644 --- a/res/layout/all_apps_personal_work_tabs.xml +++ b/res/layout/all_apps_personal_work_tabs.xml @@ -16,19 +16,23 @@ + style="@style/TextHeadline" + launcher:alignOnIcon="true">