diff --git a/Android.mk b/Android.mk index d41e184f0a..a63ab05c28 100644 --- a/Android.mk +++ b/Android.mk @@ -16,6 +16,18 @@ LOCAL_PATH := $(call my-dir) +# +# Prebuilt Java Libraries +# +include $(CLEAR_VARS) +LOCAL_MODULE := libSharedSystemUI +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_SRC_FILES := quickstep/libs/sysui_shared.jar +LOCAL_UNINSTALLABLE_MODULE := true +LOCAL_SDK_VERSION := current +include $(BUILD_PREBUILT) + # # Build rule for Launcher3 app. # @@ -24,21 +36,26 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-v4 \ + android-support-annotations + +LOCAL_STATIC_ANDROID_LIBRARIES := \ + android-support-compat \ + android-support-media-compat \ + android-support-core-utils \ + android-support-core-ui \ + android-support-fragment \ android-support-v7-recyclerview \ - android-support-v7-palette \ android-support-dynamic-animation LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ - $(call all-java-files-under, src_config) \ + $(call all-java-files-under, src_ui_overrides) \ $(call all-java-files-under, src_flags) \ $(call all-proto-files-under, protos) \ $(call all-proto-files-under, proto_overrides) -LOCAL_RESOURCE_DIR := \ - $(LOCAL_PATH)/res \ - prebuilts/sdk/current/support/v7/recyclerview/res \ + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res LOCAL_PROGUARD_FLAG_FILES := proguard.flags @@ -46,9 +63,7 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ --proto_path=$(LOCAL_PATH)/proto_overrides/ LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java -LOCAL_AAPT_FLAGS := \ - --auto-add-overlay \ - --extra-packages android.support.v7.recyclerview \ +LOCAL_USE_AAPT2 := true LOCAL_SDK_VERSION := current LOCAL_MIN_SDK_VERSION := 21 @@ -70,14 +85,20 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-v4 \ + android-support-annotations + +LOCAL_STATIC_ANDROID_LIBRARIES := \ + android-support-compat \ + android-support-media-compat \ + android-support-core-utils \ + android-support-core-ui \ + android-support-fragment \ android-support-v7-recyclerview \ - android-support-v7-palette \ android-support-dynamic-animation LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ - $(call all-java-files-under, src_config) \ + $(call all-java-files-under, src_ui_overrides) \ $(call all-java-files-under, go/src_flags) \ $(call all-proto-files-under, protos) \ $(call all-proto-files-under, proto_overrides) @@ -85,7 +106,6 @@ LOCAL_SRC_FILES := \ LOCAL_RESOURCE_DIR := \ $(LOCAL_PATH)/go/res \ $(LOCAL_PATH)/res \ - prebuilts/sdk/current/support/v7/recyclerview/res \ LOCAL_PROGUARD_FLAG_FILES := proguard.flags @@ -93,9 +113,7 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ --proto_path=$(LOCAL_PATH)/proto_overrides/ LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java -LOCAL_AAPT_FLAGS := \ - --auto-add-overlay \ - --extra-packages android.support.v7.recyclerview \ +LOCAL_USE_AAPT2 := true LOCAL_SDK_VERSION := current LOCAL_MIN_SDK_VERSION := 21 @@ -114,22 +132,111 @@ LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* include $(BUILD_PACKAGE) # -# Launcher proto buffer jar used for development +# Build rule for Quickstep app. # include $(CLEAR_VARS) -LOCAL_SRC_FILES := $(call all-proto-files-under, protos) $(call all-proto-files-under, proto_overrides) +LOCAL_MODULE_TAGS := optional + +LOCAL_STATIC_JAVA_LIBRARIES := \ + android-support-annotations \ + libSharedSystemUI + +LOCAL_STATIC_ANDROID_LIBRARIES := \ + android-support-compat \ + android-support-media-compat \ + android-support-core-utils \ + android-support-core-ui \ + android-support-fragment \ + android-support-v7-recyclerview \ + android-support-dynamic-animation + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-java-files-under, quickstep/src) \ + $(call all-java-files-under, src_flags) \ + $(call all-proto-files-under, protos) \ + $(call all-proto-files-under, proto_overrides) + +LOCAL_RESOURCE_DIR := \ + $(LOCAL_PATH)/quickstep/res \ + $(LOCAL_PATH)/res \ + +LOCAL_PROGUARD_ENABLED := disabled LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ --proto_path=$(LOCAL_PATH)/proto_overrides/ LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java -LOCAL_MODULE_TAGS := optional -LOCAL_MODULE := launcher_proto_lib -LOCAL_IS_HOST_MODULE := true -LOCAL_STATIC_JAVA_LIBRARIES := host-libprotobuf-java-nano +LOCAL_USE_AAPT2 := true + +LOCAL_SDK_VERSION := system_current +LOCAL_MIN_SDK_VERSION := 26 +LOCAL_PACKAGE_NAME := Launcher3QuickStep +LOCAL_PRIVILEGED_MODULE := true +LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 + +LOCAL_FULL_LIBS_MANIFEST_FILES := \ + $(LOCAL_PATH)/AndroidManifest.xml \ + $(LOCAL_PATH)/AndroidManifest-common.xml + +LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml +LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* + +include $(BUILD_PACKAGE) + +# +# Build rule for Launcher3 Go app with quickstep for Android Go devices. +# +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_STATIC_JAVA_LIBRARIES := \ + android-support-v4 \ + android-support-v7-recyclerview \ + android-support-dynamic-animation \ + libSharedSystemUI + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-java-files-under, quickstep/src) \ + $(call all-java-files-under, go/src_flags) \ + $(call all-proto-files-under, protos) \ + $(call all-proto-files-under, proto_overrides) + +LOCAL_RESOURCE_DIR := \ + $(LOCAL_PATH)/quickstep/res \ + $(LOCAL_PATH)/go/res \ + $(LOCAL_PATH)/res \ + prebuilts/sdk/current/support/v7/recyclerview/res \ + +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_PROTOC_OPTIMIZE_TYPE := nano +LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ --proto_path=$(LOCAL_PATH)/proto_overrides/ +LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java + +LOCAL_AAPT_FLAGS := \ + --auto-add-overlay \ + --extra-packages android.support.v7.recyclerview \ + +LOCAL_SDK_VERSION := system_current +LOCAL_MIN_SDK_VERSION := 26 +LOCAL_PACKAGE_NAME := Launcher3QuickStepGo +LOCAL_PRIVILEGED_MODULE := true +LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 + +LOCAL_FULL_LIBS_MANIFEST_FILES := \ + $(LOCAL_PATH)/go/AndroidManifest.xml \ + $(LOCAL_PATH)/AndroidManifest.xml \ + $(LOCAL_PATH)/AndroidManifest-common.xml + +LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml +LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.* + +include $(BUILD_PACKAGE) -include $(BUILD_HOST_JAVA_LIBRARY) # ================================================== include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index ad404c09a9..211e1ff37f 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -20,7 +20,6 @@ - - diff --git a/build.gradle b/build.gradle index 886ccace68..4ae6600ee3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,31 @@ buildscript { repositories { mavenCentral() - jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0' + classpath 'com.android.tools.build:gradle:3.2.0-alpha12' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3' } } +final String SUPPORT_LIBS_VERSION = '28.0.0-SNAPSHOT' + apply plugin: 'com.android.application' apply plugin: 'com.google.protobuf' android { - compileSdkVersion 26 - buildToolsVersion '26.0.0' + compileSdkVersion 28 + buildToolsVersion '28.0.0' defaultConfig { minSdkVersion 21 - targetSdkVersion 26 + targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true } buildTypes { debug { @@ -30,17 +33,40 @@ android { } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + flavorDimensions "default" + productFlavors { aosp { + dimension "default" applicationId 'com.android.launcher3' testApplicationId 'com.android.launcher3.tests' } l3go { + dimension "default" + applicationId 'com.android.launcher3' + testApplicationId 'com.android.launcher3.tests' + } + + quickstep { + dimension "default" applicationId 'com.android.launcher3' testApplicationId 'com.android.launcher3.tests' } } + + // Disable release builds for now + android.variantFilter { variant -> + if (variant.buildType.name.endsWith('release')) { + variant.setIgnore(true); + } + } + sourceSets { main { res.srcDirs = ['res'] @@ -52,55 +78,61 @@ android { } } + debug { + manifest.srcFile "AndroidManifest.xml" + } + androidTest { res.srcDirs = ['tests/res'] java.srcDirs = ['tests/src'] manifest.srcFile "tests/AndroidManifest-common.xml" } - aosp { - java.srcDirs = ['src_flags'] - manifest.srcFile "AndroidManifest.xml" + androidTestDebug { + manifest.srcFile "tests/AndroidManifest.xml" } - aospAndroidTest { - manifest.srcFile "tests/AndroidManifest.xml" + aosp { + java.srcDirs = ['src_flags', "src_ui_overrides"] } l3go { res.srcDirs = ['go/res'] - java.srcDirs = ['go/src_flags'] - // Note: we are using the Launcher3 manifest here because the gradle manifest-merger uses - // different attributes than the build system. - manifest.srcFile "AndroidManifest.xml" + java.srcDirs = ['go/src_flags', "src_ui_overrides"] + manifest.srcFile "go/AndroidManifest.xml" } - l3goAndroidTest { - manifest.srcFile "tests/AndroidManifest.xml" + quickstep { + res.srcDirs = ['quickstep/res'] + java.srcDirs = ['src_flags', 'quickstep/src'] + manifest.srcFile "quickstep/AndroidManifest.xml" } } } repositories { + maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" } + maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" } mavenCentral() - jcenter() + google() } -final String SUPPORT_LIBS_VERSION = '26.0.0-SNAPSHOT' dependencies { - compile "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}" - compile "com.android.support:support-dynamic-animation:${SUPPORT_LIBS_VERSION}" - compile "com.android.support:recyclerview-v7:${SUPPORT_LIBS_VERSION}" - compile "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}" - compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7' + implementation "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:support-dynamic-animation:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:recyclerview-v7:${SUPPORT_LIBS_VERSION}" + implementation 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7' - testCompile 'junit:junit:4.12' - androidTestCompile "org.mockito:mockito-core:1.9.5" - androidTestCompile 'com.google.dexmaker:dexmaker:1.2' - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' - androidTestCompile 'com.android.support.test:runner:0.5' - androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' - androidTestCompile "com.android.support:support-annotations:${SUPPORT_LIBS_VERSION}" + quickstepImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar') + + testImplementation 'junit:junit:4.12' + androidTestImplementation "org.mockito:mockito-core:1.9.5" + androidTestImplementation 'com.google.dexmaker:dexmaker:1.2' + androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2' + androidTestImplementation 'com.android.support.test:runner:1.0.0' + androidTestImplementation 'com.android.support.test:rules:1.0.0' + androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' + androidTestImplementation "com.android.support:support-annotations:${SUPPORT_LIBS_VERSION}" } protobuf { diff --git a/go/res/values-ar/strings.xml b/go/res/values-ar/strings.xml index 2b3b80746f..9888d0f7ca 100644 --- a/go/res/values-ar/strings.xml +++ b/go/res/values-ar/strings.xml @@ -20,7 +20,7 @@ "المس مع الاستمرار لاختيار اختصار." - "يمكنك النقر نقرًا مزدوجًا مع الاستمرار لاختيار اختصار أو استخدام الإجراءات المخصصة." + "يمكنك النقر مرّتين مع الاستمرار لاختيار اختصار أو استخدام الإجراءات المخصصة." "الاختصارات" "اختصارات %1$s" diff --git a/go/res/values-as/strings.xml b/go/res/values-as/strings.xml new file mode 100644 index 0000000000..6b5807f97b --- /dev/null +++ b/go/res/values-as/strings.xml @@ -0,0 +1,26 @@ + + + + + "কোনো শ্বৰ্টকাট বাছনি কৰিবলৈ স্পৰ্শ কৰি ৰাখক।" + "কোনো শ্বৰ্টকাট বাছনি কৰিবলৈ দুবাৰ টিপি ৰাখক বা নিজৰ উপযোগিতা অনুসৰি বনোৱা কাৰ্যসমূহ ব্যৱহাৰ কৰক।" + "শ্বৰ্টকাটসমূহ" + "%1$s শ্বৰ্টকাটসমূহ" + diff --git a/go/res/values-be-rBY/strings.xml b/go/res/values-be-rBY/strings.xml new file mode 100644 index 0000000000..4189e35fa0 --- /dev/null +++ b/go/res/values-be-rBY/strings.xml @@ -0,0 +1,26 @@ + + + + + "Дакраніцеся і ўтрымлiвайце ярлык, каб дадаць яго." + "Дакраніцеся двойчы і ўтрымлівайце, каб выбраць ярлык або выкарыстоўваць спецыяльныя дзеянні." + "Ярлыкі" + "Ярлыкі %1$s" + diff --git a/go/res/values-bn-rBD/strings.xml b/go/res/values-bn-rBD/strings.xml new file mode 100644 index 0000000000..c56c925a29 --- /dev/null +++ b/go/res/values-bn-rBD/strings.xml @@ -0,0 +1,26 @@ + + + + + "কোনও শর্টকাট বেছে নিতে টাচ করে ধরে রাখুন।" + "কোনও শর্টকাট বেছে নিতে ডাবল ট্যাপ করে ধরে রাখুন অথবা কাস্টম ক্রিয়াগুলি ব্যবহার করুন।" + "শর্টকাট" + "%1$s এর শর্টকাট" + diff --git a/go/res/values-bs-rBA/strings.xml b/go/res/values-bs-rBA/strings.xml index 7042468b47..3141b9d8fd 100644 --- a/go/res/values-bs-rBA/strings.xml +++ b/go/res/values-bs-rBA/strings.xml @@ -20,7 +20,7 @@ "Dodirnite i držite da uzmete prečicu." - "Dvaput dodirnite i držite da uzmete prečicu ili koristite prilagođene akcije." + "Dodirnite dvaput i držite da uzmete prečicu ili koristite prilagođene akcije." "Prečice" "Prečice aplikacije %1$s" diff --git a/go/res/values-bs/strings.xml b/go/res/values-bs/strings.xml index 7042468b47..3141b9d8fd 100644 --- a/go/res/values-bs/strings.xml +++ b/go/res/values-bs/strings.xml @@ -20,7 +20,7 @@ "Dodirnite i držite da uzmete prečicu." - "Dvaput dodirnite i držite da uzmete prečicu ili koristite prilagođene akcije." + "Dodirnite dvaput i držite da uzmete prečicu ili koristite prilagođene akcije." "Prečice" "Prečice aplikacije %1$s" diff --git a/go/res/values-et-rEE/strings.xml b/go/res/values-et-rEE/strings.xml new file mode 100644 index 0000000000..2513e65a33 --- /dev/null +++ b/go/res/values-et-rEE/strings.xml @@ -0,0 +1,26 @@ + + + + + "Otsetee valimiseks puudutage seda pikalt." + "Topeltpuudutage ja hoidke otsetee valimiseks või kohandatud toimingute kasutamiseks." + "Otseteed" + "Rakenduse %1$s otseteed" + diff --git a/go/res/values-eu-rES/strings.xml b/go/res/values-eu-rES/strings.xml new file mode 100644 index 0000000000..9949ef091d --- /dev/null +++ b/go/res/values-eu-rES/strings.xml @@ -0,0 +1,26 @@ + + + + + "Eduki sakatuta lasterbide bat aukeratzeko." + "Sakatu birritan eta eduki sakatuta lasterbide bat aukeratzeko edo ekintza pertsonalizatuak erabiltzeko." + "Lasterbideak" + "%1$s aplikazioaren lasterbidea" + diff --git a/go/res/values-fa/strings.xml b/go/res/values-fa/strings.xml index 8bc5256d16..f1584d9ff7 100644 --- a/go/res/values-fa/strings.xml +++ b/go/res/values-fa/strings.xml @@ -20,7 +20,7 @@ "برای انتخاب یک میان‌بر، لمس کنید و نگه‌دارید." - "برای انتخاب یک میان‌بر، دو ضربه سریع بزنید و نگه‌دارید یا از اقدام‌های سفارشی استفاده کنید." + "برای انتخاب میان‌بر، دو ضربه سریع بزنید و نگه دارید یا از کنش‌های سفارشی استفاده کنید." "میان‌برها" "میان‌برهای %1$s" diff --git a/go/res/values-gl-rES/strings.xml b/go/res/values-gl-rES/strings.xml new file mode 100644 index 0000000000..31621d5c2c --- /dev/null +++ b/go/res/values-gl-rES/strings.xml @@ -0,0 +1,26 @@ + + + + + "Mantén premido un atallo para seleccionalo." + "Toca dúas veces e mantén premido para seleccionar un atallo ou utiliza accións personalizadas." + "Atallos" + "Atallos da aplicación %1$s" + diff --git a/go/res/values-gu-rIN/strings.xml b/go/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..bdb549ff7f --- /dev/null +++ b/go/res/values-gu-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "એક શૉર્ટકટ ચૂંટવા ટૅપ કરી રાખો." + "એક શૉર્ટકટ ચૂંટવા અથવા કોઈ કસ્ટમ ક્રિયાઓનો ઉપયોગ કરવા માટે બે વાર ટૅપ કરી રાખો." + "શૉર્ટકટ" + "%1$s શૉર્ટકટ" + diff --git a/go/res/values-hi/strings.xml b/go/res/values-hi/strings.xml index bc057760fb..2c1650a0dd 100644 --- a/go/res/values-hi/strings.xml +++ b/go/res/values-hi/strings.xml @@ -20,7 +20,7 @@ "शॉर्टकट चुनने के लिए छूकर रखें." - "शॉर्टकट चुनने के लिए डबल टैप करके रखें या कस्टम कार्रवाइयों का उपयोग करें." + "शॉर्टकट चुनने के लिए दो बार छूएं और कुछ देर दबाएं रखें या अपने मुताबिक कार्रवाइयों का इस्तेमाल करें." "शॉर्टकट" "%1$s शॉर्टकट" diff --git a/go/res/values-hy-rAM/strings.xml b/go/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..4747f6df35 --- /dev/null +++ b/go/res/values-hy-rAM/strings.xml @@ -0,0 +1,26 @@ + + + + + "Կրկնակի հպեք և պահեք՝ դյուրանցում ընտրելու համար։" + "Կրկնակի հպեք և պահեք՝ դյուրանցում ընտրելու համար կամ օգտվեք հարմարեցրած գործողություններից:" + "Դյուրանցումներ" + "%1$s դյուրանցումներ" + diff --git a/go/res/values-is-rIS/strings.xml b/go/res/values-is-rIS/strings.xml new file mode 100644 index 0000000000..b8bb923746 --- /dev/null +++ b/go/res/values-is-rIS/strings.xml @@ -0,0 +1,26 @@ + + + + + "Haltu fingri á flýtileið til að grípa hana." + "Ýttu tvisvar og haltu fingri á flýtileið til að grípa hana eða notaðu sérsniðnar aðgerðir." + "Flýtileiðir" + "%1$s flýtileiðir" + diff --git a/go/res/values-kk-rKZ/strings.xml b/go/res/values-kk-rKZ/strings.xml new file mode 100644 index 0000000000..e909818afc --- /dev/null +++ b/go/res/values-kk-rKZ/strings.xml @@ -0,0 +1,26 @@ + + + + + "Таңбашаны таңдау үшін оны түртіп, ұстап тұрыңыз." + "Таңбашаны таңдау немесе арнаулы әрекеттерді пайдалану үшін екі рет түртіп, ұстап тұрыңыз." + "Таңбашалар" + "%1$s таңбаша" + diff --git a/go/res/values-km-rKH/strings.xml b/go/res/values-km-rKH/strings.xml new file mode 100644 index 0000000000..40082a4b9d --- /dev/null +++ b/go/res/values-km-rKH/strings.xml @@ -0,0 +1,26 @@ + + + + + "ប៉ះ ហើយចុចឲ្យជាប់ដើម្បីរើសផ្លូវកាត់មួយ។" + "ប៉ះពីរដង ហើយចុចឱ្យជាប់ដើម្បីរើសផ្លូវកាត់មួយ ឬប្រើសកម្មភាពផ្ទាល់ខ្លួន។" + "ផ្លូវកាត់" + "ផ្លូវកាត់សម្រាប់ %1$s" + diff --git a/go/res/values-kn-rIN/strings.xml b/go/res/values-kn-rIN/strings.xml new file mode 100644 index 0000000000..9c121fd294 --- /dev/null +++ b/go/res/values-kn-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "ಸ್ಪರ್ಶಿಸಿ ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್ ಆರಿಸಲು ಹೋಲ್ಡ್ ಮಾಡಿ." + "ಡಬಲ್ ಟ್ಯಾಪ್ ಮಾಡಿ ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್ ಆರಿಸಿಕೊಳ್ಳಲು ಹೋಲ್ಡ್ ಮಾಡಿ ಅಥವಾ ಕಸ್ಟಮ್ ಕ್ರಿಯೆಗಳನ್ನು ಬಳಸಿ." + "ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು" + "%1$s ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು" + diff --git a/go/res/values-mk-rMK/strings.xml b/go/res/values-mk-rMK/strings.xml new file mode 100644 index 0000000000..52d66b5e28 --- /dev/null +++ b/go/res/values-mk-rMK/strings.xml @@ -0,0 +1,26 @@ + + + + + "Допрете двапати и задржете за да изберете кратенка." + "Допрете двапати и задржете за да изберете кратенка или да користите приспособени дејства." + "Кратенки" + "Кратенки за %1$s" + diff --git a/go/res/values-ml-rIN/strings.xml b/go/res/values-ml-rIN/strings.xml new file mode 100644 index 0000000000..b3c12e16e1 --- /dev/null +++ b/go/res/values-ml-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "ഒരു കുറുക്കുവഴി ചേർക്കുന്നതിന് അത് സ്‌പർശിച്ച് പിടിക്കുക." + "ഒരു കുറുക്കുവഴി തിരഞ്ഞെടുക്കാനോ ഇഷ്ടാനുസൃത പ്രവർത്തനങ്ങൾ ഉപയോഗിക്കാനോ രണ്ടുതവണ ടാപ്പുചെയ്ത് പിടിക്കുക." + "കുറുക്കുവഴികൾ" + "%1$s കുറുക്കുവഴികൾ" + diff --git a/go/res/values-mn-rMN/strings.xml b/go/res/values-mn-rMN/strings.xml new file mode 100644 index 0000000000..c89dfd1bdc --- /dev/null +++ b/go/res/values-mn-rMN/strings.xml @@ -0,0 +1,26 @@ + + + + + "Товчлол авах бол удаан дарна уу." + "Товчлол авах болон тохируулсан үйлдлийг ашиглахын тулд хоёр товшоод хүлээнэ үү." + "Товчлол" + "%1$s-н товчлол" + diff --git a/go/res/values-mr-rIN/strings.xml b/go/res/values-mr-rIN/strings.xml new file mode 100644 index 0000000000..2c767b4f19 --- /dev/null +++ b/go/res/values-mr-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "शॉर्टकट निवडण्यासाठी स्पर्श करा आणि धरून ठेवा." + "शॉर्टकट निवडण्यासाठी किंवा कस्टम क्रिया वापरण्यासाठी दोनदा टॅप करा आणि धरून ठेवा." + "शॉर्टकट" + "%1$s शॉर्टकट" + diff --git a/go/res/values-ms-rMY/strings.xml b/go/res/values-ms-rMY/strings.xml new file mode 100644 index 0000000000..42add9a197 --- /dev/null +++ b/go/res/values-ms-rMY/strings.xml @@ -0,0 +1,26 @@ + + + + + "Sentuh & tahan untuk mengambil pintasan." + "Ketik dua kali & tahan untuk mengambil pintasan atau menggunakan tindakan tersuai." + "Pintasan" + "Pintasan %1$s" + diff --git a/go/res/values-my-rMM/strings.xml b/go/res/values-my-rMM/strings.xml new file mode 100644 index 0000000000..5784df63bf --- /dev/null +++ b/go/res/values-my-rMM/strings.xml @@ -0,0 +1,26 @@ + + + + + "လက်ကွက်ဖြတ်လမ်းတစ်ခုကို ရွေးရန် ထိပြီး ဖိထားပါ" + "လက်ကွက်ဖြတ်လမ်းကို ရွေးရန် (သို့) စိတ်ကြိုက်လုပ်ဆောင်ချက်များကို သုံးရန်နှစ်ချက်တို့ပြီး ဖိထားပါ။" + "ဖြတ်လမ်းလင့်ခ်များ" + "%1$s ဖြတ်လမ်းလင့်ခ်များ" + diff --git a/go/res/values-ne-rNP/strings.xml b/go/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..0be0375f3a --- /dev/null +++ b/go/res/values-ne-rNP/strings.xml @@ -0,0 +1,26 @@ + + + + + "कुनै सटकर्ट छनौट गर्न छोइराख्नुहोस्।" + "कुनै सर्टकट छनौट गर्न दुईपटक ट्याप गरेर होल्ड गर्नुहोस् वा रोजेका कारबाहीहरू प्रयोग गर्नुहोस्।" + "सर्टकटहरू" + "%1$s सर्टकटहरू" + diff --git a/go/res/values-or/strings.xml b/go/res/values-or/strings.xml new file mode 100644 index 0000000000..3ec8a72038 --- /dev/null +++ b/go/res/values-or/strings.xml @@ -0,0 +1,26 @@ + + + + + "ଏକ ଶର୍ଟକଟ୍ ଚୟନ କରିବାକୁ ଦାବି ଧରନ୍ତୁ।" + "ଡବଲ୍‌-ଟାପ୍‌ କରନ୍ତୁ ଏବଂ ଏକ ଶର୍ଟକଟ୍ ଚୟନ କରିବାକୁ ଧରି ରଖନ୍ତୁ କିମ୍ୱା କଷ୍ଟମ୍ ପ୍ରକ୍ରିୟା ବ୍ୟବହାର କରନ୍ତୁ।" + "ଶର୍ଟକଟ୍‍" + "%1$sର ଶର୍ଟକଟ୍" + diff --git a/go/res/values-pa-rIN/strings.xml b/go/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..c7e4abf3a8 --- /dev/null +++ b/go/res/values-pa-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "ਕੋਈ ਸ਼ਾਰਟਕੱਟ ਚੁਣਨ ਲਈ ਸਪੱਰਸ਼ ਕਰੋ ਅਤੇ ਦਬਾ ਕੇ ਰੱਖੋ।" + "ਕੋਈ ਸ਼ਾਰਟਕੱਟ ਚੁਣਨ ਲਈ ਦੋ ਵਾਰ ਟੈਪ ਕਰੋ ਅਤੇ ਦਬਾ ਕੇ ਰੱਖੋ ਜਾਂ ਵਿਉਂਂਤੀ ਕਾਰਵਾਈਆਂ ਵਰਤੋ।" + "ਸ਼ਾਰਟਕੱਟ" + "%1$s ਸ਼ਾਰਟਕੱਟ" + diff --git a/go/res/values-pa/strings.xml b/go/res/values-pa/strings.xml index f3982ab71b..c7e4abf3a8 100644 --- a/go/res/values-pa/strings.xml +++ b/go/res/values-pa/strings.xml @@ -20,7 +20,7 @@ "ਕੋਈ ਸ਼ਾਰਟਕੱਟ ਚੁਣਨ ਲਈ ਸਪੱਰਸ਼ ਕਰੋ ਅਤੇ ਦਬਾ ਕੇ ਰੱਖੋ।" - "ਕੋਈ ਸ਼ਾਰਟਕੱਟ ਚੁਣਨ ਲਈ ਡਬਲ-ਟੈਪ ਕਰੋ ਅਤੇ ਦਬਾ ਕੇ ਰੱਖੋ ਜਾਂ ਵਿਸ਼ੇਸ਼-ਵਿਉਂਤਬੱਧ ਕਾਰਵਾਈਆਂ ਵਰਤੋ।" + "ਕੋਈ ਸ਼ਾਰਟਕੱਟ ਚੁਣਨ ਲਈ ਦੋ ਵਾਰ ਟੈਪ ਕਰੋ ਅਤੇ ਦਬਾ ਕੇ ਰੱਖੋ ਜਾਂ ਵਿਉਂਂਤੀ ਕਾਰਵਾਈਆਂ ਵਰਤੋ।" "ਸ਼ਾਰਟਕੱਟ" "%1$s ਸ਼ਾਰਟਕੱਟ" diff --git a/go/res/values-sq-rAL/strings.xml b/go/res/values-sq-rAL/strings.xml new file mode 100644 index 0000000000..bb74db6b55 --- /dev/null +++ b/go/res/values-sq-rAL/strings.xml @@ -0,0 +1,26 @@ + + + + + "Prek dhe mbaj prekur për të zgjedhur një shkurtore." + "Prek dy herë dhe mbaj prekur për të zgjedhur një shkurtore ose për të përdorur veprimet e personalizuara." + "Shkurtoret" + "%1$s shkurtore" + diff --git a/go/res/values-sw/strings.xml b/go/res/values-sw/strings.xml index 0379ed012a..13c12e4a6c 100644 --- a/go/res/values-sw/strings.xml +++ b/go/res/values-sw/strings.xml @@ -20,7 +20,7 @@ "Gusa na ushikilie ili uchague njia ya mkato." - "Gonga mara mbili na ushikilie ile uchague njia ya mkato au utumie vitendo maalum." + "Gusa mara mbili na ushikilie ile uchague njia ya mkato au utumie vitendo maalum." "Njia za mkato" "Njia za mkato za %1$s" diff --git a/go/res/values-ta-rIN/strings.xml b/go/res/values-ta-rIN/strings.xml new file mode 100644 index 0000000000..50059b65f9 --- /dev/null +++ b/go/res/values-ta-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "குறுக்குவழியைச் சேர்க்க, தொட்டு பிடித்திருக்கவும்." + "குறுக்குவழியை சேர்க்க, இருமுறை தட்டிப் பிடித்திருக்கவும் அல்லது தனிப்பயன் செயல்களைப் பயன்படுத்தவும்." + "குறுக்குவழிகள்" + "%1$s குறுக்குவழிகள்" + diff --git a/go/res/values-te-rIN/strings.xml b/go/res/values-te-rIN/strings.xml new file mode 100644 index 0000000000..0bdf743342 --- /dev/null +++ b/go/res/values-te-rIN/strings.xml @@ -0,0 +1,26 @@ + + + + + "సత్వరమార్గాన్ని ఎంచుకోవడానికి తాకి & నొక్కి ఉంచండి." + "సత్వరమార్గాన్ని ఎంచుకోవడానికి లేదా అనుకూల చర్యలను ఉపయోగించడానికి రెండుసార్లు నొక్కి &ఉంచండి." + "సత్వరమార్గాలు" + "%1$s సత్వరమార్గాలు" + diff --git a/go/res/values-ur-rPK/strings.xml b/go/res/values-ur-rPK/strings.xml new file mode 100644 index 0000000000..46bd823d8e --- /dev/null +++ b/go/res/values-ur-rPK/strings.xml @@ -0,0 +1,26 @@ + + + + + "کوئی شارٹ کٹ منتخب کرنے کیلئے ٹچ کریں اور دبائے رکھیں۔" + "کوئی شارٹ کٹ منتخب کرنے یا حسب ضرورت کاروائیاں استعمال کرنے کیلئے دو بار تھپتھپائیں اور دبائے رکھیں۔" + "شارٹ کٹس" + "%1$s شارٹ کٹس" + diff --git a/go/res/xml/device_profiles.xml b/go/res/xml/device_profiles.xml index 487c026d6d..16d7e13cb9 100644 --- a/go/res/xml/device_profiles.xml +++ b/go/res/xml/device_profiles.xml @@ -15,7 +15,7 @@ limitations under the License. --> - + (...); } +# The support library contains references to newer platform versions. +# Don't warn about those in case this app is linking against an older +# platform version. We know about them, and they are safe. +-dontwarn android.support.** + # Proguard will strip methods required for talkback to properly scroll to # next row when focus is on the last item of last row when using a RecyclerView # Keep optimized and shrunk proguard to prevent issues like this when using # support jar. -#-keep,allowoptimization,allowshrinking class android.support.** { -# *; -#} -keep class android.support.v7.widget.RecyclerView { *; } +# LauncherAppTransitionManager +-keep class com.android.launcher3.LauncherAppTransitionManagerImpl { + public (...); +} + +# InstantAppResolver +-keep class com.android.quickstep.InstantAppResolverImpl { + public (...); +} + +# MainProcessInitializer +-keep class com.android.quickstep.QuickstepProcessInitializer { + public (...); +} + +# UserEventDispatcherExtension +-keep class com.android.quickstep.logging.UserEventDispatcherExtension { + public (...); +} + -keep interface com.android.launcher3.userevent.nano.LauncherLogProto.** { *; } @@ -102,3 +124,14 @@ -keep interface com.android.launcher3.model.nano.LauncherDumpProto.** { *; } + +# Discovery bounce animation +-keep class com.android.launcher3.allapps.DiscoveryBounce$VerticalProgressWrapper { + public void setProgress(float); + public float getProgress(); +} + +# BUG(70852369): Surpress additional warnings after changing from Proguard to R8 +-dontwarn android.app.** +-dontwarn android.view.** +-dontwarn android.os.** diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto index 0bbec188db..06e6a923d4 100644 --- a/protos/launcher_log.proto +++ b/protos/launcher_log.proto @@ -55,6 +55,7 @@ message Target { optional int32 span_y = 14 [default = 1];// Used for ItemType.WIDGET optional int32 predictedRank = 15; optional TargetExtension extension = 16; + optional TipType tip_type = 17; } // Used to define what type of item a Target would represent. @@ -68,6 +69,8 @@ enum ItemType { SEARCHBOX = 6; EDITTEXT = 7; NOTIFICATION = 8; + TASK = 9; // Each page of Recents UI (QuickStep) + WEB_APP = 10; } // Used to define what type of container a Target would represent. @@ -78,11 +81,16 @@ enum ContainerType { FOLDER = 3; ALLAPPS = 4; WIDGETS = 5; - OVERVIEW = 6; + OVERVIEW = 6; // Zoomed out workspace (without QuickStep) PREDICTION = 7; SEARCHRESULT = 8; DEEPSHORTCUTS = 9; PINITEM = 10; // confirmation screen + NAVBAR = 11; + TASKSWITCHER = 12; // Recents UI Container (QuickStep) + APP = 13; // Foreground activity is another app (QuickStep) + TIP = 14; // Onboarding texts (QuickStep) + SIDELOADED_LAUNCHER = 15; } // Used to define what type of control a Target would represent. @@ -99,7 +107,19 @@ enum ControlType { VERTICAL_SCROLL = 9; HOME_INTENT = 10; // Deprecated, use enum Command instead BACK_BUTTON = 11; // Deprecated, use enum Command instead - // GO_TO_PLAYSTORE + QUICK_SCRUB_BUTTON = 12; + CLEAR_ALL_BUTTON = 13; + CANCEL_TARGET = 14; + TASK_PREVIEW = 15; + SPLIT_SCREEN_TARGET = 16; +} + +enum TipType { + DEFAULT_NONE = 0; + BOUNCE = 1; + SWIPE_UP_TEXT = 2; + QUICK_SCRUB_TEXT = 3; + PREDICTION_TEXT = 4; } // Used to define the action component of the LauncherEvent. @@ -108,8 +128,10 @@ message Action { TOUCH = 0; AUTOMATED = 1; COMMAND = 2; + TIP = 3; // SOFT_KEYBOARD, HARD_KEYBOARD, ASSIST } + enum Touch { TAP = 0; LONGPRESS = 1; @@ -118,7 +140,8 @@ message Action { FLING = 4; PINCH = 5; } - enum Direction { + + enum Direction { NONE = 0; UP = 1; DOWN = 2; @@ -128,17 +151,22 @@ message Action { enum Command { HOME_INTENT = 0; BACK = 1; - ENTRY = 2; // Indicates entry to one of Launcher container type target - // not using the HOME_INTENT - CANCEL = 3; // Indicates that a confirmation screen was cancelled - CONFIRM = 4; // Indicates thata confirmation screen was accepted + ENTRY = 2; // Indicates entry to one of Launcher container type target + // not using the HOME_INTENT + CANCEL = 3; // Indicates that a confirmation screen was cancelled + CONFIRM = 4; // Indicates thata confirmation screen was accepted + STOP = 5; // Indicates onStop() was called (screen time out, power off) + RECENTS_BUTTON = 6; // Indicates that Recents button was pressed + RESUME = 7; // Indicates onResume() was called } + optional Type type = 1; optional Touch touch = 2; optional Direction dir = 3; optional Command command = 4; // Log if the action was performed on outside of the container optional bool is_outside = 5; + optional bool is_state_change = 6; } // @@ -148,7 +176,6 @@ message Action { // message LauncherEvent { required Action action = 1; - // List of targets that touch actions can be operated on. repeated Target src_target = 2; repeated Target dest_target = 3; diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml new file mode 100644 index 0000000000..778866d5ea --- /dev/null +++ b/quickstep/AndroidManifest.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar new file mode 100644 index 0000000000..53a6ceb4d0 Binary files /dev/null and b/quickstep/libs/sysui_shared.jar differ diff --git a/quickstep/res/drawable/bg_workspace_card_button.xml b/quickstep/res/drawable/bg_workspace_card_button.xml new file mode 100644 index 0000000000..3ba47bb4d1 --- /dev/null +++ b/quickstep/res/drawable/bg_workspace_card_button.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/quickstep/res/drawable/ic_empty_recents.xml b/quickstep/res/drawable/ic_empty_recents.xml new file mode 100644 index 0000000000..5183733975 --- /dev/null +++ b/quickstep/res/drawable/ic_empty_recents.xml @@ -0,0 +1,30 @@ + + + + diff --git a/quickstep/res/drawable/ic_pin.xml b/quickstep/res/drawable/ic_pin.xml new file mode 100644 index 0000000000..8c799e38ec --- /dev/null +++ b/quickstep/res/drawable/ic_pin.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/ic_split_screen.xml b/quickstep/res/drawable/ic_split_screen.xml new file mode 100644 index 0000000000..77bd3336d2 --- /dev/null +++ b/quickstep/res/drawable/ic_split_screen.xml @@ -0,0 +1,45 @@ + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/fallback_recents_activity.xml b/quickstep/res/layout/fallback_recents_activity.xml new file mode 100644 index 0000000000..84e13add58 --- /dev/null +++ b/quickstep/res/layout/fallback_recents_activity.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml new file mode 100644 index 0000000000..25615e0e2d --- /dev/null +++ b/quickstep/res/layout/overview_clear_all_button.xml @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml new file mode 100644 index 0000000000..840b040ac0 --- /dev/null +++ b/quickstep/res/layout/overview_panel.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/layout/zzz_dummy_widget.xml b/quickstep/res/layout/scrim_view.xml similarity index 61% rename from res/layout/zzz_dummy_widget.xml rename to quickstep/res/layout/scrim_view.xml index a0fa8fc3e4..2cc37f9cc4 100644 --- a/res/layout/zzz_dummy_widget.xml +++ b/quickstep/res/layout/scrim_view.xml @@ -1,5 +1,5 @@ - - - - - - - - - \ No newline at end of file + android:id="@+id/scrim_view" /> \ No newline at end of file diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml new file mode 100644 index 0000000000..f163872161 --- /dev/null +++ b/quickstep/res/layout/task.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/shortcuts_item.xml b/quickstep/res/layout/task_menu.xml similarity index 53% rename from res/layout/shortcuts_item.xml rename to quickstep/res/layout/task_menu.xml index 7cd996de01..b8466652e5 100644 --- a/res/layout/shortcuts_item.xml +++ b/quickstep/res/layout/task_menu.xml @@ -1,5 +1,6 @@ - - - - - - - - - + - - - + android:layout_height="112dp" + android:textSize="14sp" + android:paddingTop="18dp" + android:drawablePadding="8dp" + android:gravity="center_horizontal"/> + \ No newline at end of file diff --git a/quickstep/res/values-af/strings.xml b/quickstep/res/values-af/strings.xml new file mode 100644 index 0000000000..8ae493fb6e --- /dev/null +++ b/quickstep/res/values-af/strings.xml @@ -0,0 +1,29 @@ + + + + + "Verdeelde skerm" + "Speld vas" + "Oorsig" + "Geen onlangse items nie" + "Maak toe" + "Vee alles uit" + "Onlangse programme" + diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml new file mode 100644 index 0000000000..4ca0c67979 --- /dev/null +++ b/quickstep/res/values-am/strings.xml @@ -0,0 +1,29 @@ + + + + + "የተከፈለ ማያ ገጽ" + "ሰካ" + "ማጠቃለያ" + "ምንም የቅርብ ጊዜ ንጥሎች የሉም" + "ዝጋ" + "ሁሉንም አጽዳ" + "የቅርብ ጊዜ መተግበሪያዎች" + diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml new file mode 100644 index 0000000000..c04b618699 --- /dev/null +++ b/quickstep/res/values-ar/strings.xml @@ -0,0 +1,29 @@ + + + + + "تقسيم الشاشة" + "تثبيت" + "نظرة عامة" + "ليست هناك عناصر تم استخدامها مؤخرًا" + "إغلاق" + "محو الكل" + "التطبيقات التي تمّ استخدامها مؤخرًا" + diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml new file mode 100644 index 0000000000..812a24649a --- /dev/null +++ b/quickstep/res/values-as/strings.xml @@ -0,0 +1,29 @@ + + + + + "বিভাজিত স্ক্ৰীণ" + "পিন" + "অৱলোকন" + "কোনো শেহতীয়া বস্তু নাই" + "বন্ধ কৰক" + "সকলো মচক" + "শেহতীয়া এপসমূহ" + diff --git a/quickstep/res/values-az-rAZ/strings.xml b/quickstep/res/values-az-rAZ/strings.xml new file mode 100644 index 0000000000..0546f46508 --- /dev/null +++ b/quickstep/res/values-az-rAZ/strings.xml @@ -0,0 +1,28 @@ + + + + + "Bölünmüş ekran" + "Sancın" + "İcmal" + "Son elementlər yoxdur" + "Bağlayın" + "Hamısını silin" + diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml new file mode 100644 index 0000000000..832ed8c5f6 --- /dev/null +++ b/quickstep/res/values-az/strings.xml @@ -0,0 +1,29 @@ + + + + + "Bölünmüş ekran" + "Sancın" + "İcmal" + "Son elementlər yoxdur" + "Bağlayın" + "Hamısını silin" + "Son tətbiqlər" + diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..fc4c8c0d08 --- /dev/null +++ b/quickstep/res/values-b+sr+Latn/strings.xml @@ -0,0 +1,29 @@ + + + + + "Podeljeni ekran" + "Zakači" + "Pregled" + "Nema nedavnih stavki" + "Zatvori" + "Obriši sve" + "Nedavne aplikacije" + diff --git a/quickstep/res/values-be-rBY/strings.xml b/quickstep/res/values-be-rBY/strings.xml new file mode 100644 index 0000000000..1e60dd36fd --- /dev/null +++ b/quickstep/res/values-be-rBY/strings.xml @@ -0,0 +1,28 @@ + + + + + "Падзяліць экран" + "Замацаваць" + "Агляд" + "Няма новых элементаў" + "Закрыць" + "Ачысціць усё" + diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml new file mode 100644 index 0000000000..c5d03d47a5 --- /dev/null +++ b/quickstep/res/values-be/strings.xml @@ -0,0 +1,29 @@ + + + + + "Падзяліць экран" + "Замацаваць" + "Агляд" + "Няма новых элементаў" + "Закрыць" + "Ачысціць усё" + "Нядаўнія праграмы" + diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml new file mode 100644 index 0000000000..2672b83653 --- /dev/null +++ b/quickstep/res/values-bg/strings.xml @@ -0,0 +1,29 @@ + + + + + "Разделен екран" + "Фиксиране" + "Общ преглед" + "Няма скорошни елементи" + "Затваряне" + "Изчистване на всички" + "Скорошни приложения" + diff --git a/quickstep/res/values-bn-rBD/strings.xml b/quickstep/res/values-bn-rBD/strings.xml new file mode 100644 index 0000000000..a0605d8317 --- /dev/null +++ b/quickstep/res/values-bn-rBD/strings.xml @@ -0,0 +1,28 @@ + + + + + "স্ক্রিন স্প্লিট করুন" + "পিন করুন" + "এক নজরে" + "কোনো সাম্প্রতিক আইটেম নেই" + "বন্ধ করুন" + "সবকিছু খালি করুন" + diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml new file mode 100644 index 0000000000..0a824c2dd1 --- /dev/null +++ b/quickstep/res/values-bn/strings.xml @@ -0,0 +1,29 @@ + + + + + "স্ক্রিন স্প্লিট করুন" + "পিন করুন" + "এক নজরে" + "কোনো সাম্প্রতিক আইটেম নেই" + "বন্ধ করুন" + "সবকিছু খালি করুন" + "সম্প্রতি ব্যবহৃত অ্যাপ" + diff --git a/quickstep/res/values-bs-rBA/strings.xml b/quickstep/res/values-bs-rBA/strings.xml new file mode 100644 index 0000000000..9ffa84806c --- /dev/null +++ b/quickstep/res/values-bs-rBA/strings.xml @@ -0,0 +1,28 @@ + + + + + "Način rada podijeljenog ekrana" + "Zakači" + "Pregled" + "Nema nedavnih stavki" + "Zatvaranje" + "Obriši sve" + diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml new file mode 100644 index 0000000000..6bf38eba15 --- /dev/null +++ b/quickstep/res/values-bs/strings.xml @@ -0,0 +1,29 @@ + + + + + "Način rada podijeljenog ekrana" + "Zakači" + "Pregled" + "Nema nedavnih stavki" + "Zatvaranje" + "Obriši sve" + "Nedavne aplikacije" + diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml new file mode 100644 index 0000000000..2d51703ae4 --- /dev/null +++ b/quickstep/res/values-ca/strings.xml @@ -0,0 +1,29 @@ + + + + + "Divideix la pantalla" + "Fixa" + "Aplicacions recents" + "No hi ha cap element recent" + "Tanca" + "Esborra-ho tot" + "Aplicacions recents" + diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml new file mode 100644 index 0000000000..a178df08b4 --- /dev/null +++ b/quickstep/res/values-cs/strings.xml @@ -0,0 +1,29 @@ + + + + + "Rozdělená obrazovka" + "PIN" + "Přehled" + "Žádné nedávné položky" + "Zavřít" + "Vymazat vše" + "Poslední aplikace" + diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml new file mode 100644 index 0000000000..d0d629f5a1 --- /dev/null +++ b/quickstep/res/values-da/strings.xml @@ -0,0 +1,29 @@ + + + + + "Delt skærm" + "Fastgør" + "Oversigt" + "Ingen nye elementer" + "Luk" + "Ryd alt" + "Seneste apps" + diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml new file mode 100644 index 0000000000..aee8b85b8c --- /dev/null +++ b/quickstep/res/values-de/strings.xml @@ -0,0 +1,29 @@ + + + + + "Bildschirm teilen" + "Fixieren" + "Übersicht" + "Keine kürzlich verwendeten Elemente" + "Schließen" + "Alle Apps schließen" + "Zuletzt aktive Apps" + diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml new file mode 100644 index 0000000000..7364b82823 --- /dev/null +++ b/quickstep/res/values-el/strings.xml @@ -0,0 +1,29 @@ + + + + + "Διαχωρισμός οθόνης" + "Καρφίτσωμα" + "Επισκόπηση" + "Δεν υπάρχουν πρόσφατα στοιχεία" + "Κλείσιμο" + "Διαγραφή όλων" + "Πρόσφατες εφαρμογές" + diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml new file mode 100644 index 0000000000..d0dc1e8cb8 --- /dev/null +++ b/quickstep/res/values-en-rAU/strings.xml @@ -0,0 +1,29 @@ + + + + + "Split screen" + "Pin" + "Overview" + "No recent items" + "Close" + "Clear all" + "Recent apps" + diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..d0dc1e8cb8 --- /dev/null +++ b/quickstep/res/values-en-rGB/strings.xml @@ -0,0 +1,29 @@ + + + + + "Split screen" + "Pin" + "Overview" + "No recent items" + "Close" + "Clear all" + "Recent apps" + diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml new file mode 100644 index 0000000000..d0dc1e8cb8 --- /dev/null +++ b/quickstep/res/values-en-rIN/strings.xml @@ -0,0 +1,29 @@ + + + + + "Split screen" + "Pin" + "Overview" + "No recent items" + "Close" + "Clear all" + "Recent apps" + diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml new file mode 100644 index 0000000000..83e9d96aa1 --- /dev/null +++ b/quickstep/res/values-es-rUS/strings.xml @@ -0,0 +1,29 @@ + + + + + "Pantalla dividida" + "Fijar" + "Recientes" + "No hay elementos recientes" + "Cerrar" + "Borrar todo" + "Apps recientes" + diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml new file mode 100644 index 0000000000..e076df56ef --- /dev/null +++ b/quickstep/res/values-es/strings.xml @@ -0,0 +1,29 @@ + + + + + "Dividir pantalla" + "Fijar" + "Aplicaciones recientes" + "No hay elementos recientes" + "Cerrar" + "Borrar todo" + "Aplicaciones recientes" + diff --git a/quickstep/res/values-et-rEE/strings.xml b/quickstep/res/values-et-rEE/strings.xml new file mode 100644 index 0000000000..efa68f9c43 --- /dev/null +++ b/quickstep/res/values-et-rEE/strings.xml @@ -0,0 +1,28 @@ + + + + + "Jagatud ekraan" + "Kinnita" + "Ülevaade" + "Hiljutisi üksusi pole" + "Sule" + "Sule kõik" + diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml new file mode 100644 index 0000000000..85de9c0ab7 --- /dev/null +++ b/quickstep/res/values-et/strings.xml @@ -0,0 +1,29 @@ + + + + + "Jagatud ekraan" + "Kinnita" + "Ülevaade" + "Hiljutisi üksusi pole" + "Sule" + "Sule kõik" + "Hiljutised rakendused" + diff --git a/quickstep/res/values-eu-rES/strings.xml b/quickstep/res/values-eu-rES/strings.xml new file mode 100644 index 0000000000..d22242efab --- /dev/null +++ b/quickstep/res/values-eu-rES/strings.xml @@ -0,0 +1,28 @@ + + + + + "Zatitu pantaila" + "Ainguratu" + "Ikuspegi orokorra" + "Ez dago azkenaldi honetako ezer" + "Itxi" + "Garbitu guztiak" + diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml new file mode 100644 index 0000000000..60943ccc4f --- /dev/null +++ b/quickstep/res/values-eu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Zatitu pantaila" + "Ainguratu" + "Ikuspegi orokorra" + "Ez dago azkenaldi honetako ezer" + "Itxi" + "Garbitu guztiak" + "Azken aplikazioak" + diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml new file mode 100644 index 0000000000..4a30daa30c --- /dev/null +++ b/quickstep/res/values-fa/strings.xml @@ -0,0 +1,29 @@ + + + + + "تقسیم صفحه" + "پین" + "نمای کلی" + "بدون موارد اخیر" + "بستن" + "پاک کردن همه" + "برنامه‌های اخیر" + diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml new file mode 100644 index 0000000000..8f4135001f --- /dev/null +++ b/quickstep/res/values-fi/strings.xml @@ -0,0 +1,29 @@ + + + + + "Jaettu näyttö" + "Kiinnitä" + "Viimeisimmät" + "Ei viimeaikaisia kohteita" + "Sulje" + "Poista kaikki" + "Viimeisimmät sovellukset" + diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000000..7177996415 --- /dev/null +++ b/quickstep/res/values-fr-rCA/strings.xml @@ -0,0 +1,29 @@ + + + + + "Écran divisé" + "Épingler" + "Aperçu" + "Aucun élément récent" + "Fermer" + "Tout effacer" + "Applications récentes" + diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml new file mode 100644 index 0000000000..0deb00bc35 --- /dev/null +++ b/quickstep/res/values-fr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Écran partagé" + "Épingler" + "Aperçu" + "Aucun élément récent" + "Fermer" + "Tout effacer" + "Applications récentes" + diff --git a/quickstep/res/values-gl-rES/strings.xml b/quickstep/res/values-gl-rES/strings.xml new file mode 100644 index 0000000000..8efc773f49 --- /dev/null +++ b/quickstep/res/values-gl-rES/strings.xml @@ -0,0 +1,28 @@ + + + + + "Pantalla dividida" + "Fixar" + "Visión xeral" + "Non hai elementos recentes" + "Pecha a aplicación" + "Borrar todo" + diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml new file mode 100644 index 0000000000..8c217eb7bb --- /dev/null +++ b/quickstep/res/values-gl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Pantalla dividida" + "Fixar" + "Visión xeral" + "Non hai elementos recentes" + "Pecha a aplicación" + "Borrar todo" + "Aplicacións recentes" + diff --git a/quickstep/res/values-gu-rIN/strings.xml b/quickstep/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..cdae86feba --- /dev/null +++ b/quickstep/res/values-gu-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "સ્ક્રીનને વિભાજિત કરો" + "પિન કરો" + "ઝલક" + "તાજેતરની કોઈ આઇટમ નથી" + "બંધ કરો" + "બધું સાફ કરો" + diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml new file mode 100644 index 0000000000..8b9a538ed1 --- /dev/null +++ b/quickstep/res/values-gu/strings.xml @@ -0,0 +1,29 @@ + + + + + "સ્ક્રીનને વિભાજિત કરો" + "પિન કરો" + "ઝલક" + "તાજેતરની કોઈ આઇટમ નથી" + "બંધ કરો" + "બધું સાફ કરો" + "તાજેતરની ઍપ" + diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml new file mode 100644 index 0000000000..83c31e78da --- /dev/null +++ b/quickstep/res/values-hi/strings.xml @@ -0,0 +1,29 @@ + + + + + "स्क्रीन को दो हिस्सों में बाँटना (स्प्लिट स्क्रीन)" + "पिन करना" + "खास जानकारी" + "हाल ही में इस्तेमाल किया गया कोई ऐप्लिकेशन नहीं है" + "बंद करें" + "सभी ऐप्लिकेशन बंद करें" + "हाल ही में इस्तेमाल किए गए एेप्लिकेशन" + diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml new file mode 100644 index 0000000000..baa8d85acb --- /dev/null +++ b/quickstep/res/values-hr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Podijeljeni zaslon" + "Prikvači" + "Pregled" + "Nema nedavnih stavki" + "Zatvori" + "Izbriši sve" + "Nedavne aplikacije" + diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml new file mode 100644 index 0000000000..d971374774 --- /dev/null +++ b/quickstep/res/values-hu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Osztott képernyő" + "Rögzítés" + "Áttekintés" + "Nincsenek mostanában használt elemek" + "Bezárás" + "Összes törlése" + "Legutóbbi alkalmazások" + diff --git a/quickstep/res/values-hy-rAM/strings.xml b/quickstep/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..4afae7de7d --- /dev/null +++ b/quickstep/res/values-hy-rAM/strings.xml @@ -0,0 +1,28 @@ + + + + + "Տրոհել էկրանը" + "Ամրացնել" + "Ընդհանուր տեղեկություններ" + "Վերջին տարրեր չկան" + "Փակել" + "Փակել բոլորը" + diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml new file mode 100644 index 0000000000..b4b9b98095 --- /dev/null +++ b/quickstep/res/values-hy/strings.xml @@ -0,0 +1,29 @@ + + + + + "Տրոհել էկրանը" + "Ամրացնել" + "Ընդհանուր տեղեկություններ" + "Վերջին տարրեր չկան" + "Փակել" + "Փակել բոլորը" + "Վերջին օգտագործած հավելվածները" + diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml new file mode 100644 index 0000000000..787c14338e --- /dev/null +++ b/quickstep/res/values-in/strings.xml @@ -0,0 +1,29 @@ + + + + + "Layar terpisah" + "Pasang pin" + "Ringkasan" + "Tidak ada item yang baru dibuka" + "Tutup" + "Hapus semua" + "Aplikasi baru-baru ini" + diff --git a/quickstep/res/values-is-rIS/strings.xml b/quickstep/res/values-is-rIS/strings.xml new file mode 100644 index 0000000000..88a92ed96e --- /dev/null +++ b/quickstep/res/values-is-rIS/strings.xml @@ -0,0 +1,28 @@ + + + + + "Skipta skjá" + "Festa" + "Yfirlit" + "Engin nýleg atriði" + "Loka" + "Hreinsa allt" + diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml new file mode 100644 index 0000000000..1aface0c90 --- /dev/null +++ b/quickstep/res/values-is/strings.xml @@ -0,0 +1,29 @@ + + + + + "Skipta skjá" + "Festa" + "Yfirlit" + "Engin nýleg atriði" + "Loka" + "Hreinsa allt" + "Nýleg forrit" + diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml new file mode 100644 index 0000000000..192ec44c72 --- /dev/null +++ b/quickstep/res/values-it/strings.xml @@ -0,0 +1,29 @@ + + + + + "Schermo diviso" + "Blocca" + "Panoramica" + "Nessun elemento recente" + "Chiudi" + "Cancella tutto" + "App recenti" + diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml new file mode 100644 index 0000000000..64c35ec116 --- /dev/null +++ b/quickstep/res/values-iw/strings.xml @@ -0,0 +1,29 @@ + + + + + "מסך מפוצל" + "הצמדה" + "מסכים אחרונים" + "אין פריטים אחרונים" + "סגירה" + "ניקוי הכול" + "אפליקציות אחרונות" + diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml new file mode 100644 index 0000000000..558112cded --- /dev/null +++ b/quickstep/res/values-ja/strings.xml @@ -0,0 +1,29 @@ + + + + + "分割画面" + "固定" + "概要" + "最近のアイテムはありません" + "閉じる" + "すべてクリア" + "最近使ったアプリ" + diff --git a/quickstep/res/values-ka-rGE/strings.xml b/quickstep/res/values-ka-rGE/strings.xml new file mode 100644 index 0000000000..6de8ed9222 --- /dev/null +++ b/quickstep/res/values-ka-rGE/strings.xml @@ -0,0 +1,28 @@ + + + + + "ეკრანის გაყოფა" + "ჩამაგრება" + "მიმოხილვა" + "ბოლოს გამოყენებული ერთეულები არ არის" + "დახურვა" + "ყველას გასუფთავება" + diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml new file mode 100644 index 0000000000..5f061debb8 --- /dev/null +++ b/quickstep/res/values-ka/strings.xml @@ -0,0 +1,29 @@ + + + + + "ეკრანის გაყოფა" + "ჩამაგრება" + "მიმოხილვა" + "ბოლოს გამოყენებული ერთეულები არ არის" + "დახურვა" + "ყველას გასუფთავება" + "ბოლოდროინდელი აპები" + diff --git a/quickstep/res/values-kk-rKZ/strings.xml b/quickstep/res/values-kk-rKZ/strings.xml new file mode 100644 index 0000000000..ddd4a77196 --- /dev/null +++ b/quickstep/res/values-kk-rKZ/strings.xml @@ -0,0 +1,28 @@ + + + + + "Экранды бөлу" + "Бекіту" + "Шолу" + "Соңғы элементтер жоқ" + "Жабу" + "Барлығын өшіру" + diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml new file mode 100644 index 0000000000..3e72130f1f --- /dev/null +++ b/quickstep/res/values-kk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Экранды бөлу" + "Бекіту" + "Шолу" + "Соңғы элементтер жоқ" + "Жабу" + "Барлығын өшіру" + "Соңғы пайдаланылған қолданбалар" + diff --git a/quickstep/res/values-km-rKH/strings.xml b/quickstep/res/values-km-rKH/strings.xml new file mode 100644 index 0000000000..65c1dccee3 --- /dev/null +++ b/quickstep/res/values-km-rKH/strings.xml @@ -0,0 +1,28 @@ + + + + + "មុខងារ​បំបែកអេក្រង់" + "ដៅ" + "ទិដ្ឋភាពរួម" + "មិនមានធាតុថ្មីៗទេ" + "បិទ" + "សម្អាត​ទាំងអស់" + diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml new file mode 100644 index 0000000000..6ae948258b --- /dev/null +++ b/quickstep/res/values-km/strings.xml @@ -0,0 +1,29 @@ + + + + + "មុខងារ​បំបែកអេក្រង់" + "ដៅ" + "ទិដ្ឋភាពរួម" + "មិនមានធាតុថ្មីៗទេ" + "បិទ" + "សម្អាត​ទាំងអស់" + "កម្មវិធី​ថ្មីៗ" + diff --git a/quickstep/res/values-kn-rIN/strings.xml b/quickstep/res/values-kn-rIN/strings.xml new file mode 100644 index 0000000000..55ccbb0260 --- /dev/null +++ b/quickstep/res/values-kn-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "ಪರದೆಯನ್ನು ಬೇರ್ಪಡಿಸಿ" + "ಪಿನ್ ಮಾಡಿ" + "ಅವಲೋಕನ" + "ಯಾವುದೇ ಇತ್ತೀಚಿನ ಐಟಂಗಳಿಲ್ಲ" + "ಮುಚ್ಚಿ" + "ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ" + diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml new file mode 100644 index 0000000000..591418b9c2 --- /dev/null +++ b/quickstep/res/values-kn/strings.xml @@ -0,0 +1,29 @@ + + + + + "ಪರದೆಯನ್ನು ಬೇರ್ಪಡಿಸಿ" + "ಪಿನ್ ಮಾಡಿ" + "ಅವಲೋಕನ" + "ಯಾವುದೇ ಇತ್ತೀಚಿನ ಐಟಂಗಳಿಲ್ಲ" + "ಮುಚ್ಚಿ" + "ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ" + "ಇತ್ತೀಚಿನ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು" + diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml new file mode 100644 index 0000000000..5b1ecbd2e7 --- /dev/null +++ b/quickstep/res/values-ko/strings.xml @@ -0,0 +1,29 @@ + + + + + "화면 분할" + "고정" + "최근 사용" + "최근 항목이 없습니다." + "닫기" + "모두 삭제" + "최근 앱" + diff --git a/quickstep/res/values-ky-rKG/strings.xml b/quickstep/res/values-ky-rKG/strings.xml new file mode 100644 index 0000000000..b788693824 --- /dev/null +++ b/quickstep/res/values-ky-rKG/strings.xml @@ -0,0 +1,28 @@ + + + + + "Экранды бөлүү" + "Кадап коюу" + "Сереп салуу" + "Акыркы колдонмолор жок" + "Жабуу" + "Баарын тазалоо" + diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml new file mode 100644 index 0000000000..9f84037f7b --- /dev/null +++ b/quickstep/res/values-ky/strings.xml @@ -0,0 +1,29 @@ + + + + + "Экранды бөлүү" + "Кадап коюу" + "Сереп салуу" + "Акыркы колдонмолор жок" + "Жабуу" + "Баарын тазалоо" + "Акыркы колдонмолор" + diff --git a/quickstep/res/values-lo-rLA/strings.xml b/quickstep/res/values-lo-rLA/strings.xml new file mode 100644 index 0000000000..a83743a7e9 --- /dev/null +++ b/quickstep/res/values-lo-rLA/strings.xml @@ -0,0 +1,28 @@ + + + + + "ແບ່ງໜ້າຈໍ" + "ປັກໝຸດ" + "ພາບຮວມ" + "ບໍ່ມີລາຍການຫຼ້າສຸດ" + "ປິດ" + "ລຶບລ້າງທັງໝົດ" + diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml new file mode 100644 index 0000000000..ed1cb6e849 --- /dev/null +++ b/quickstep/res/values-lo/strings.xml @@ -0,0 +1,29 @@ + + + + + "ແບ່ງໜ້າຈໍ" + "ປັກໝຸດ" + "ພາບຮວມ" + "ບໍ່ມີລາຍການຫຼ້າສຸດ" + "ປິດ" + "ລຶບລ້າງທັງໝົດ" + "ແອັບຫຼ້າສຸດ" + diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml new file mode 100644 index 0000000000..c16fc56bbd --- /dev/null +++ b/quickstep/res/values-lt/strings.xml @@ -0,0 +1,29 @@ + + + + + "Skaidyti ekraną" + "Prisegti" + "Apžvalga" + "Nėra jokių naujausių elementų" + "Uždaryti" + "Išvalyti viską" + "Naujausios programos" + diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml new file mode 100644 index 0000000000..2dee41c43a --- /dev/null +++ b/quickstep/res/values-lv/strings.xml @@ -0,0 +1,29 @@ + + + + + "Sadalīt ekrānu" + "Piespraust" + "Pārskats" + "Nav nesenu vienumu." + "Aizvērt" + "Notīrīt visu" + "Pēdējās izmantotās lietotnes" + diff --git a/quickstep/res/values-mk-rMK/strings.xml b/quickstep/res/values-mk-rMK/strings.xml new file mode 100644 index 0000000000..e428b4e1f8 --- /dev/null +++ b/quickstep/res/values-mk-rMK/strings.xml @@ -0,0 +1,28 @@ + + + + + "Поделен екран" + "Прикачување" + "Преглед" + "Нема неодамнешни ставки" + "Затвори" + "Исчисти ги сите" + diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml new file mode 100644 index 0000000000..ff16cea8cb --- /dev/null +++ b/quickstep/res/values-mk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Поделен екран" + "Прикачување" + "Преглед" + "Нема неодамнешни ставки" + "Затвори" + "Исчисти ги сите" + "Неодамнешни апликации" + diff --git a/quickstep/res/values-ml-rIN/strings.xml b/quickstep/res/values-ml-rIN/strings.xml new file mode 100644 index 0000000000..4cca447e7d --- /dev/null +++ b/quickstep/res/values-ml-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "സ്‌ക്രീൻ വിഭജിക്കുക" + "പിൻ ചെയ്യുക" + "അവലോകനം" + "സമീപകാല ഇനങ്ങൾ ഒന്നുമില്ല" + "അവസാനിപ്പിക്കുക" + "എല്ലാം മായ്‌ക്കുക" + diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml new file mode 100644 index 0000000000..624aded2a3 --- /dev/null +++ b/quickstep/res/values-ml/strings.xml @@ -0,0 +1,29 @@ + + + + + "സ്‌ക്രീൻ വിഭജിക്കുക" + "പിൻ ചെയ്യുക" + "അവലോകനം" + "സമീപകാല ഇനങ്ങൾ ഒന്നുമില്ല" + "അവസാനിപ്പിക്കുക" + "എല്ലാം മായ്‌ക്കുക" + "സമീപകാല ആപ്പുകൾ" + diff --git a/quickstep/res/values-mn-rMN/strings.xml b/quickstep/res/values-mn-rMN/strings.xml new file mode 100644 index 0000000000..f40f69e315 --- /dev/null +++ b/quickstep/res/values-mn-rMN/strings.xml @@ -0,0 +1,28 @@ + + + + + "Дэлгэцийг хуваах" + "Тогтоох" + "Тойм" + "Сүүлийн үеийн зүйл алга" + "Хаах" + "Бүгдийг устгах" + diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml new file mode 100644 index 0000000000..f9021252bd --- /dev/null +++ b/quickstep/res/values-mn/strings.xml @@ -0,0 +1,29 @@ + + + + + "Дэлгэцийг хуваах" + "Тогтоох" + "Тойм" + "Сүүлийн үеийн зүйл алга" + "Хаах" + "Бүгдийг устгах" + "Саяхны аппууд" + diff --git a/quickstep/res/values-mr-rIN/strings.xml b/quickstep/res/values-mr-rIN/strings.xml new file mode 100644 index 0000000000..938363db0f --- /dev/null +++ b/quickstep/res/values-mr-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "विभाजित स्क्रीन" + "पिन करा" + "अवलोकन" + "कोणतेही अलीकडील आयटम नाहीत" + "बंद" + "सर्व साफ करा" + diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml new file mode 100644 index 0000000000..7a669dd647 --- /dev/null +++ b/quickstep/res/values-mr/strings.xml @@ -0,0 +1,29 @@ + + + + + "विभाजित स्क्रीन" + "पिन करा" + "अवलोकन" + "कोणतेही अलीकडील आयटम नाहीत" + "बंद" + "सर्व साफ करा" + "अलीकडील अॅप्स" + diff --git a/quickstep/res/values-ms-rMY/strings.xml b/quickstep/res/values-ms-rMY/strings.xml new file mode 100644 index 0000000000..236fab25c8 --- /dev/null +++ b/quickstep/res/values-ms-rMY/strings.xml @@ -0,0 +1,28 @@ + + + + + "Skrin pisah" + "Semat" + "Ikhtisar" + "Tiada item terbaharu" + "Tutup" + "Kosongkan semua" + diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml new file mode 100644 index 0000000000..699586373c --- /dev/null +++ b/quickstep/res/values-ms/strings.xml @@ -0,0 +1,29 @@ + + + + + "Skrin pisah" + "Semat" + "Ikhtisar" + "Tiada item terbaharu" + "Tutup" + "Kosongkan semua" + "Apl terbaharu" + diff --git a/quickstep/res/values-my-rMM/strings.xml b/quickstep/res/values-my-rMM/strings.xml new file mode 100644 index 0000000000..e44b904beb --- /dev/null +++ b/quickstep/res/values-my-rMM/strings.xml @@ -0,0 +1,28 @@ + + + + + "မျက်နှာပြင် ခွဲ၍ပြသခြင်း" + "ပင်ထိုးခြင်း" + "အနှစ်ချုပ်" + "မကြာမီကဖွင့်ထားသည်များ မရှိပါ" + "ပိတ်ရန်" + "အားလုံးကို ရှင်းရန်" + diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml new file mode 100644 index 0000000000..ae6dc7d0a0 --- /dev/null +++ b/quickstep/res/values-my/strings.xml @@ -0,0 +1,29 @@ + + + + + "မျက်နှာပြင် ခွဲ၍ပြသခြင်း" + "ပင်ထိုးခြင်း" + "အနှစ်ချုပ်" + "မကြာမီကဖွင့်ထားသည်များ မရှိပါ" + "ပိတ်ရန်" + "အားလုံးကို ရှင်းရန်" + "လတ်တလောသုံး အက်ပ်များ" + diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml new file mode 100644 index 0000000000..cb8ee10f8e --- /dev/null +++ b/quickstep/res/values-nb/strings.xml @@ -0,0 +1,29 @@ + + + + + "Delt skjerm" + "Fest" + "Oversikt" + "Ingen nylige elementer" + "Lukk" + "Fjern alt" + "Nylige apper" + diff --git a/quickstep/res/values-ne-rNP/strings.xml b/quickstep/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..bf52604fd2 --- /dev/null +++ b/quickstep/res/values-ne-rNP/strings.xml @@ -0,0 +1,28 @@ + + + + + "स्क्रिन विभाजन गर्नुहोस्" + "पिन गर्नुहोस्" + "परिदृश्य" + "हालसालैको कुनै पनि वस्तु छैन" + "बन्द गर्नुहोस्" + "सबै खाली गर्नुहोस्" + diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml new file mode 100644 index 0000000000..4efae7b080 --- /dev/null +++ b/quickstep/res/values-ne/strings.xml @@ -0,0 +1,29 @@ + + + + + "स्क्रिन विभाजन गर्नुहोस्" + "पिन गर्नुहोस्" + "परिदृश्य" + "हालसालैको कुनै पनि वस्तु छैन" + "बन्द गर्नुहोस्" + "सबै खाली गर्नुहोस्" + "हालसालैका अनुप्रयोगहरू" + diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml new file mode 100644 index 0000000000..8ef2a5d5ce --- /dev/null +++ b/quickstep/res/values-nl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Gesplitst scherm" + "Vastzetten" + "Overzicht" + "Geen recente items" + "Sluiten" + "Alles wissen" + "Recente apps" + diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml new file mode 100644 index 0000000000..6895ef5f5f --- /dev/null +++ b/quickstep/res/values-or/strings.xml @@ -0,0 +1,29 @@ + + + + + "ସ୍କ୍ରୀନ୍‌କୁ ଭାଗ କରନ୍ତୁ" + "ପିନ୍‍" + "ସଂକ୍ଷିପ୍ତ ବିବରଣ" + "କୌଣସି ସାମ୍ପ୍ରତିକ ଆଇଟମ୍ ନାହିଁ" + "ବନ୍ଦ କରନ୍ତୁ" + "ସବୁ ଖାଲି କରନ୍ତୁ" + "ସାମ୍ପ୍ରତିକ ଆପ୍‌" + diff --git a/quickstep/res/values-pa-rIN/strings.xml b/quickstep/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..bc044c808f --- /dev/null +++ b/quickstep/res/values-pa-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "ਸਪਲਿਟ ਸਕ੍ਰੀਨ" + "ਪਿੰਨ ਕਰੋ" + "ਰੂਪ-ਰੇਖਾ" + "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮਾਂ ਨਹੀਂ" + "ਬੰਦ ਕਰੋ" + "ਸਭ ਕਲੀਅਰ ਕਰੋ" + diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml new file mode 100644 index 0000000000..4159c30b3f --- /dev/null +++ b/quickstep/res/values-pa/strings.xml @@ -0,0 +1,29 @@ + + + + + "ਸਪਲਿਟ ਸਕ੍ਰੀਨ" + "ਪਿੰਨ ਕਰੋ" + "ਰੂਪ-ਰੇਖਾ" + "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮਾਂ ਨਹੀਂ" + "ਬੰਦ ਕਰੋ" + "ਸਭ ਕਲੀਅਰ ਕਰੋ" + "ਹਾਲੀਆ ਐਪਾਂ" + diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml new file mode 100644 index 0000000000..cf15abd171 --- /dev/null +++ b/quickstep/res/values-pl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Podziel ekran" + "Przypnij" + "Przegląd" + "Brak ostatnich elementów" + "Zamknij" + "Wyczyść wszystko" + "Ostatnie aplikacje" + diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..36c7e3c455 --- /dev/null +++ b/quickstep/res/values-pt-rPT/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ecrã dividido" + "Fixar" + "Vista geral" + "Nenhum item recente" + "Fechar" + "Limpar tudo" + "Aplicações recentes" + diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml new file mode 100644 index 0000000000..41f53f0e98 --- /dev/null +++ b/quickstep/res/values-pt/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tela dividida" + "Fixar" + "Visão geral" + "Nenhum item recente" + "Fechar" + "Limpar tudo" + "Apps recentes" + diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml new file mode 100644 index 0000000000..032d886fb1 --- /dev/null +++ b/quickstep/res/values-ro/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ecran divizat" + "Fixați" + "Recente" + "Niciun element recent" + "Închideți" + "Ștergeți tot" + "Aplicații recente" + diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml new file mode 100644 index 0000000000..6a218fc6b8 --- /dev/null +++ b/quickstep/res/values-ru/strings.xml @@ -0,0 +1,29 @@ + + + + + "Разделить экран" + "Блокировать" + "Обзор" + "Здесь пока ничего нет." + "Закрыть" + "Очистить все" + "Недавние приложения" + diff --git a/quickstep/res/values-si-rLK/strings.xml b/quickstep/res/values-si-rLK/strings.xml new file mode 100644 index 0000000000..61bb8ba81c --- /dev/null +++ b/quickstep/res/values-si-rLK/strings.xml @@ -0,0 +1,28 @@ + + + + + "බෙදුම් තිරය" + "අමුණන්න" + "දළ විශ්ලේෂණය" + "මෑත අයිතම නැත" + "වසන්න" + "සියල්ල හිස් කරන්න" + diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml new file mode 100644 index 0000000000..c01211ac51 --- /dev/null +++ b/quickstep/res/values-si/strings.xml @@ -0,0 +1,29 @@ + + + + + "බෙදුම් තිරය" + "අමුණන්න" + "දළ විශ්ලේෂණය" + "මෑත අයිතම නැත" + "වසන්න" + "සියල්ල හිස් කරන්න" + "මෑත යෙදුම්" + diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml new file mode 100644 index 0000000000..2cd394208e --- /dev/null +++ b/quickstep/res/values-sk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Rozdeliť obrazovku" + "Pripnúť" + "Prehľad" + "Žiadne nedávne položky" + "Zavrieť" + "Vymazať všetko" + "Nedávne aplikácie" + diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml new file mode 100644 index 0000000000..06eb95d863 --- /dev/null +++ b/quickstep/res/values-sl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Razdeljen zaslon" + "Pripni" + "Pregled" + "Ni nedavnih elementov" + "Zapri" + "Počisti vse" + "Nedavne aplikacije" + diff --git a/quickstep/res/values-sq-rAL/strings.xml b/quickstep/res/values-sq-rAL/strings.xml new file mode 100644 index 0000000000..a0c3d781d0 --- /dev/null +++ b/quickstep/res/values-sq-rAL/strings.xml @@ -0,0 +1,28 @@ + + + + + "Ekrani i ndarë" + "Gozhdo" + "Përmbledhja" + "Nuk ka asnjë artikull të fundit" + "Mbyll" + "Pastroji të gjitha" + diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml new file mode 100644 index 0000000000..00231e1340 --- /dev/null +++ b/quickstep/res/values-sq/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ekrani i ndarë" + "Gozhdo" + "Përmbledhja" + "Nuk ka asnjë artikull të fundit" + "Mbyll" + "Pastroji të gjitha" + "Aplikacionet e fundit" + diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml new file mode 100644 index 0000000000..0077ee244c --- /dev/null +++ b/quickstep/res/values-sr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Подељени екран" + "Закачи" + "Преглед" + "Нема недавних ставки" + "Затвори" + "Обриши све" + "Недавне апликације" + diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml new file mode 100644 index 0000000000..f05d79f2af --- /dev/null +++ b/quickstep/res/values-sv/strings.xml @@ -0,0 +1,29 @@ + + + + + "Delad skärm" + "Fäst" + "Översikt" + "Listan med de senaste åtgärderna är tom" + "Stäng" + "Rensa alla" + "Senaste apparna" + diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml new file mode 100644 index 0000000000..e6ce953dbd --- /dev/null +++ b/quickstep/res/values-sw/strings.xml @@ -0,0 +1,29 @@ + + + + + "Gawa skrini" + "Bandika" + "Muhtasari" + "Hakuna vipengee vya hivi karibuni" + "Funga" + "Ondoa zote" + "Programu za hivi karibuni" + diff --git a/quickstep/res/values-ta-rIN/strings.xml b/quickstep/res/values-ta-rIN/strings.xml new file mode 100644 index 0000000000..0c800ca720 --- /dev/null +++ b/quickstep/res/values-ta-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "திரைப் பிரிப்பு" + "பின் செய்தல்" + "மேலோட்டப் பார்வை" + "சமீபத்தியவை எதுவுமில்லை" + "மூடும்" + "எல்லாம் அழி" + diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml new file mode 100644 index 0000000000..19518e1810 --- /dev/null +++ b/quickstep/res/values-ta/strings.xml @@ -0,0 +1,29 @@ + + + + + "திரைப் பிரிப்பு" + "பின் செய்தல்" + "மேலோட்டப் பார்வை" + "சமீபத்தியவை எதுவுமில்லை" + "மூடும்" + "எல்லாம் அழி" + "சமீபத்திய ஆப்ஸ்" + diff --git a/quickstep/res/values-te-rIN/strings.xml b/quickstep/res/values-te-rIN/strings.xml new file mode 100644 index 0000000000..416fbb848d --- /dev/null +++ b/quickstep/res/values-te-rIN/strings.xml @@ -0,0 +1,28 @@ + + + + + "స్క్రీన్‌ని విభజించు" + "పిన్ చేయి" + "అవలోకనం" + "ఇటీవలి అంశాలు ఏవీ లేవు" + "మూసివేయండి" + "అన్నీ తీసివేయండి" + diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml new file mode 100644 index 0000000000..4c0e5ac563 --- /dev/null +++ b/quickstep/res/values-te/strings.xml @@ -0,0 +1,29 @@ + + + + + "స్క్రీన్‌ని విభజించు" + "పిన్ చేయి" + "అవలోకనం" + "ఇటీవలి అంశాలు ఏవీ లేవు" + "మూసివేయండి" + "అన్నీ తీసివేయండి" + "ఇటీవలి యాప్‌లు" + diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml new file mode 100644 index 0000000000..8dfda248c3 --- /dev/null +++ b/quickstep/res/values-th/strings.xml @@ -0,0 +1,29 @@ + + + + + "แยกหน้าจอ" + "ตรึง" + "ภาพรวม" + "ไม่มีรายการล่าสุด" + "ปิด" + "ล้างทั้งหมด" + "แอปล่าสุด" + diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml new file mode 100644 index 0000000000..ab3cac9dc2 --- /dev/null +++ b/quickstep/res/values-tl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Hatiin ang screen" + "I-pin" + "Overview" + "Walang kamakailang item" + "Isara" + "I-clear lahat" + "Mga kamakailang app" + diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml new file mode 100644 index 0000000000..9693413034 --- /dev/null +++ b/quickstep/res/values-tr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Bölünmüş ekran" + "Sabitle" + "Genel bakış" + "Yeni öğe yok" + "Kapat" + "Tümünü temizle" + "Son uygulamalar" + diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml new file mode 100644 index 0000000000..2afcb31865 --- /dev/null +++ b/quickstep/res/values-uk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Розділити екран" + "Закріпити" + "Огляд" + "Немає нещодавніх додатків" + "Закрити" + "Очистити все" + "Нещодавні додатки" + diff --git a/quickstep/res/values-ur-rPK/strings.xml b/quickstep/res/values-ur-rPK/strings.xml new file mode 100644 index 0000000000..0a546fd603 --- /dev/null +++ b/quickstep/res/values-ur-rPK/strings.xml @@ -0,0 +1,28 @@ + + + + + "اسپلٹ اسکرین وضع" + "پن کریں" + "مجموعی جائزہ" + "کوئی حالیہ آئٹم نہیں" + "بند کریں" + "سبھی کو صاف کریں" + diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml new file mode 100644 index 0000000000..f493a2f64d --- /dev/null +++ b/quickstep/res/values-ur/strings.xml @@ -0,0 +1,29 @@ + + + + + "اسپلٹ اسکرین وضع" + "پن کریں" + "مجموعی جائزہ" + "کوئی حالیہ آئٹم نہیں" + "بند کریں" + "سبھی کو صاف کریں" + "حالیہ ایپس" + diff --git a/quickstep/res/values-uz-rUZ/strings.xml b/quickstep/res/values-uz-rUZ/strings.xml new file mode 100644 index 0000000000..efea34117f --- /dev/null +++ b/quickstep/res/values-uz-rUZ/strings.xml @@ -0,0 +1,28 @@ + + + + + "Ekranni ikkiga ajratish" + "Mahkamlash" + "Nazar" + "Yaqinda ishlatilgan ilovalar yo‘q" + "Yopish" + "Hammasini tozalash" + diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml new file mode 100644 index 0000000000..4911925984 --- /dev/null +++ b/quickstep/res/values-uz/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ekranni ikkiga ajratish" + "Mahkamlash" + "Nazar" + "Yaqinda ishlatilgan ilovalar yo‘q" + "Yopish" + "Hammasini tozalash" + "Yaqinda ishlatilgan ilovalar" + diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml new file mode 100644 index 0000000000..406faf5f8c --- /dev/null +++ b/quickstep/res/values-vi/strings.xml @@ -0,0 +1,29 @@ + + + + + "Chia đôi màn hình" + "Ghim" + "Tổng quan" + "Không có mục gần đây nào" + "Đóng" + "Xóa tất cả" + "Ứng dụng gần đây" + diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..71ac114979 --- /dev/null +++ b/quickstep/res/values-zh-rCN/strings.xml @@ -0,0 +1,29 @@ + + + + + "分屏" + "固定" + "概览" + "近期没有任何内容" + "关闭" + "全部清除" + "最近用过的应用" + diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..ab29a91bfe --- /dev/null +++ b/quickstep/res/values-zh-rHK/strings.xml @@ -0,0 +1,29 @@ + + + + + "分割畫面" + "固定" + "概覽" + "最近沒有任何項目" + "關閉" + "全部清除" + "最近使用的應用程式" + diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..1a9448c59f --- /dev/null +++ b/quickstep/res/values-zh-rTW/strings.xml @@ -0,0 +1,29 @@ + + + + + "分割畫面" + "固定" + "總覽" + "最近沒有任何項目" + "關閉" + "全部清除" + "最近使用的應用程式" + diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml new file mode 100644 index 0000000000..3d4f37250a --- /dev/null +++ b/quickstep/res/values-zu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Hlukanisa isikrini" + "Phina" + "Buka konke" + "Azikho izinto zakamuva" + "Vala" + "Sula konke" + "Izinhlelo zokusebenza zakamuva" + diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml new file mode 100644 index 0000000000..c294376340 --- /dev/null +++ b/quickstep/res/values/config.xml @@ -0,0 +1,22 @@ + + + + + + + + com.android.quickstep.logging.UserEventDispatcherExtension + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml new file mode 100644 index 0000000000..ed18bf5e3a --- /dev/null +++ b/quickstep/res/values/dimens.xml @@ -0,0 +1,55 @@ + + + + + + 24dp + 48dp + 12dp + 2dp + 10dp + 20dp + + + 600dp + + 500dp + 250dp + + + 50dp + 50dp + 115dp + + 16sp + 16dp + + + 40dp + 136dp + 200dp + 100dp + + 10dp + + + 168dp + + 16dp + diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml new file mode 100644 index 0000000000..d6836594f4 --- /dev/null +++ b/quickstep/res/values/override.xml @@ -0,0 +1,24 @@ + + + + + com.android.launcher3.LauncherAppTransitionManagerImpl + + com.android.quickstep.InstantAppResolverImpl + + com.android.quickstep.QuickstepProcessInitializer + + diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml new file mode 100644 index 0000000000..a76899d3da --- /dev/null +++ b/quickstep/res/values/strings.xml @@ -0,0 +1,44 @@ + + + + + + Quickstep + + + + Split screen + + Pin + + + Overview + + + No recent items + + + Close + + + Clear all + + + Recent apps + \ No newline at end of file diff --git a/quickstep/res/xml/indexable_launcher_prefs.xml b/quickstep/res/xml/indexable_launcher_prefs.xml new file mode 100644 index 0000000000..30f310021f --- /dev/null +++ b/quickstep/res/xml/indexable_launcher_prefs.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java new file mode 100644 index 0000000000..e34631042d --- /dev/null +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -0,0 +1,139 @@ +/* + * 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; + +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Handler; +import android.support.annotation.BinderThread; +import android.support.annotation.UiThread; + +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +@TargetApi(Build.VERSION_CODES.P) +public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { + + private final Handler mHandler; + private final boolean mStartAtFrontOfQueue; + private AnimationResult mAnimationResult; + + /** + * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the + * queue to minimize latency. + */ + public LauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue) { + mHandler = handler; + mStartAtFrontOfQueue = startAtFrontOfQueue; + } + + @BinderThread + @Override + public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) { + Runnable r = () -> { + finishExistingAnimation(); + mAnimationResult = new AnimationResult(runnable); + onCreateAnimation(targetCompats, mAnimationResult); + }; + if (mStartAtFrontOfQueue) { + postAtFrontOfQueueAsynchronously(mHandler, r); + } else { + postAsyncCallback(mHandler, r); + } + } + + /** + * Called on the UI thread when the animation targets are received. The implementation must + * call {@link AnimationResult#setAnimation(AnimatorSet)} with the target animation to be run. + */ + @UiThread + public abstract void onCreateAnimation( + RemoteAnimationTargetCompat[] targetCompats, AnimationResult result); + + @UiThread + private void finishExistingAnimation() { + if (mAnimationResult != null) { + mAnimationResult.finish(); + mAnimationResult = null; + } + } + + /** + * Called by the system + */ + @BinderThread + @Override + public void onAnimationCancelled() { + postAsyncCallback(mHandler, this::finishExistingAnimation); + } + + public static final class AnimationResult { + + private final Runnable mFinishRunnable; + + private AnimatorSet mAnimator; + private boolean mFinished = false; + private boolean mInitialized = false; + + private AnimationResult(Runnable finishRunnable) { + mFinishRunnable = finishRunnable; + } + + @UiThread + private void finish() { + if (!mFinished) { + mFinishRunnable.run(); + mFinished = true; + } + } + + @UiThread + public void setAnimation(AnimatorSet animation) { + if (mInitialized) { + throw new IllegalStateException("Animation already initialized"); + } + mInitialized = true; + mAnimator = animation; + if (mAnimator == null) { + finish(); + } else if (mFinished) { + // Animation callback was already finished, skip the animation. + mAnimator.start(); + mAnimator.end(); + } else { + // Start the animation + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finish(); + } + }); + mAnimator.start(); + + // Because t=0 has the app icon in its original spot, we can skip the + // first frame and have the same movement one frame earlier. + mAnimator.setCurrentPlayTime(SINGLE_FRAME_MS); + } + } + } +} \ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java new file mode 100644 index 0000000000..2630edb3d2 --- /dev/null +++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java @@ -0,0 +1,805 @@ +/* + * 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; + +import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +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.Utilities.postAsyncCallback; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; +import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS; +import static com.android.quickstep.TaskUtils.findTaskViewToLaunch; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber; +import static com.android.systemui.shared.recents.utilities.Utilities.getSurface; +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.annotation.TargetApi; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.Pair; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.shortcuts.DeepShortcutView; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.ActivityOptionsCompat; +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.TransactionCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +/** + * Manages the opening and closing app transitions from Launcher. + */ +@TargetApi(Build.VERSION_CODES.O) +@SuppressWarnings("unused") +public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManager + implements OnDeviceProfileChangeListener { + + private static final String TAG = "LauncherTransition"; + public static final int STATUS_BAR_TRANSITION_DURATION = 120; + + private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = + "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; + + private static final int APP_LAUNCH_DURATION = 500; + // Use a shorter duration for x or y translation to create a curve effect + private static final int APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2; + // We scale the durations for the downward app launch animations (minus the scale animation). + private static final float APP_LAUNCH_DOWN_DUR_SCALE_FACTOR = 0.8f; + private static final int APP_LAUNCH_ALPHA_START_DELAY = 32; + private static final int APP_LAUNCH_ALPHA_DURATION = 50; + + public static final int RECENTS_LAUNCH_DURATION = 336; + public static final int RECENTS_QUICKSCRUB_LAUNCH_DURATION = 300; + private static final int LAUNCHER_RESUME_START_DELAY = 100; + private static final int CLOSING_TRANSITION_DURATION_MS = 250; + + // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down. + public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f; + + private final Launcher mLauncher; + private final DragLayer mDragLayer; + private final AlphaProperty mDragLayerAlpha; + + private final Handler mHandler; + private final boolean mIsRtl; + + private final float mContentTransY; + private final float mWorkspaceTransY; + private final float mClosingWindowTransY; + + private DeviceProfile mDeviceProfile; + private View mFloatingView; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mLauncher.addForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.clearForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + }; + + public LauncherAppTransitionManagerImpl(Context context) { + mLauncher = Launcher.getLauncher(context); + mDragLayer = mLauncher.getDragLayer(); + mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS); + mHandler = new Handler(Looper.getMainLooper()); + mIsRtl = Utilities.isRtl(mLauncher.getResources()); + mDeviceProfile = mLauncher.getDeviceProfile(); + + Resources res = mLauncher.getResources(); + mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y); + mWorkspaceTransY = res.getDimensionPixelSize(R.dimen.workspace_trans_y); + mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); + + mLauncher.addOnDeviceProfileChangeListener(this); + registerRemoteAnimations(); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + mDeviceProfile = dp; + } + + /** + * @return ActivityOptions with remote animations that controls how the window of the opening + * targets are displayed. + */ + @Override + public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) { + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + AnimatorSet anim = new AnimatorSet(); + + boolean launcherClosing = + launcherIsATargetWithMode(targetCompats, MODE_CLOSING); + + if (!composeRecentsLaunchAnimator(v, targetCompats, anim)) { + // Set the state animation first so that any state listeners are called + // before our internal listeners. + mLauncher.getStateManager().setCurrentAnimation(anim); + + Rect windowTargetBounds = getWindowTargetBounds(targetCompats); + anim.play(getIconAnimator(v, windowTargetBounds)); + if (launcherClosing) { + Pair launcherContentAnimator = + getLauncherContentAnimator(true /* isAppOpening */); + anim.play(launcherContentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + launcherContentAnimator.second.run(); + } + }); + } + anim.play(getOpeningWindowAnimators(v, targetCompats, windowTargetBounds)); + } + + if (launcherClosing) { + anim.addListener(mForceInvisibleListener); + } + + result.setAnimation(anim); + } + }; + + int duration = findTaskViewToLaunch(launcher, v, null) != null + ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION; + int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, duration, statusBarTransitionDelay)); + } + return super.getActivityLaunchOptions(launcher, v); + } + + /** + * 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 + * figure out where the floating view should animate to. + */ + private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] targets) { + Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + if (mLauncher.isInMultiWindowModeCompat()) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + bounds.set(target.sourceContainerBounds); + bounds.offsetTo(target.position.x, target.position.y); + return bounds; + } + } + } + return bounds; + } + + public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider, + CancellationSignal cancellationSignal) { + mRemoteAnimationProvider = animationProvider; + cancellationSignal.setOnCancelListener(() -> { + if (animationProvider == mRemoteAnimationProvider) { + mRemoteAnimationProvider = null; + } + }); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private boolean composeRecentsLaunchAnimator(View v, + RemoteAnimationTargetCompat[] targets, AnimatorSet target) { + // Ensure recents is actually visible + if (!mLauncher.getStateManager().getState().overviewUi) { + return false; + } + + RecentsView recentsView = mLauncher.getOverviewPanel(); + boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING); + boolean skipLauncherChanges = !launcherClosing; + boolean isLaunchingFromQuickscrub = + recentsView.getQuickScrubController().isWaitingForTaskLaunch(); + + TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets); + if (taskView == null) { + return false; + } + + int duration = isLaunchingFromQuickscrub + ? RECENTS_QUICKSCRUB_LAUNCH_DURATION + : RECENTS_LAUNCH_DURATION; + + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper) + .setDuration(duration)); + + Animator childStateAnimation = null; + // Found a visible recents task that matches the opening app, lets launch the app from there + Animator launcherAnim; + final AnimatorListenerAdapter windowAnimEndListener; + if (launcherClosing) { + launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper); + launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + launcherAnim.setDuration(duration); + + // Make sure recents gets fixed up by resetting task alphas and scales, etc. + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().moveToRestState(); + mLauncher.getStateManager().reapplyState(); + } + }; + } else { + AnimatorPlaybackController controller = + mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL, duration); + controller.dispatchOnStart(); + childStateAnimation = controller.getTarget(); + launcherAnim = controller.getAnimationPlayer().setDuration(duration); + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().goToState(NORMAL, false); + } + }; + } + target.play(launcherAnim); + + // Set the current animation first, before adding windowAnimEndListener. Setting current + // animation adds some listeners which need to be called before windowAnimEndListener + // (the ordering of listeners matter in this case). + mLauncher.getStateManager().setCurrentAnimation(target, childStateAnimation); + target.addListener(windowAnimEndListener); + return true; + } + + /** + * 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. + */ + private Pair getLauncherContentAnimator(boolean isAppOpening) { + AnimatorSet launcherAnimator = new AnimatorSet(); + Runnable endListener; + + float[] alphas = isAppOpening + ? new float[] {1, 0} + : new float[] {0, 1}; + float[] trans = isAppOpening + ? new float[] {0, mContentTransY} + : new float[] {-mContentTransY, 0}; + + if (mLauncher.isInState(ALL_APPS)) { + // 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 startY = appsView.getTranslationY(); + appsView.setAlpha(alphas[0]); + appsView.setTranslationY(trans[0]); + + ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + appsView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + alpha.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + + launcherAnimator.play(alpha); + launcherAnimator.play(transY); + + endListener = () -> { + appsView.setAlpha(startAlpha); + appsView.setTranslationY(startY); + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + }; + } else if (mLauncher.isInState(OVERVIEW)) { + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + launcherAnimator.play(ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, + allAppsController.getProgress(), ALL_APPS_PROGRESS_OFF_SCREEN)); + + View overview = mLauncher.getOverviewPanelContainer(); + ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, View.ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + overview.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + endListener = () -> { + overview.setLayerType(View.LAYER_TYPE_NONE, null); + overview.setAlpha(1f); + overview.setTranslationY(0f); + mLauncher.getStateManager().reapplyState(); + }; + } else { + mDragLayerAlpha.setValue(alphas[0]); + ObjectAnimator alpha = + ObjectAnimator.ofFloat(mDragLayerAlpha, MultiValueAlpha.VALUE, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + mDragLayer.setTranslationY(trans[0]); + ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + mDragLayer.getScrim().hideSysUiScrim(true); + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + endListener = this::resetContentView; + } + return new Pair<>(launcherAnimator, endListener); + } + + /** + * @return Animator that controls the icon used to launch the target. + */ + private AnimatorSet getIconAnimator(View v, Rect windowTargetBounds) { + final boolean isBubbleTextView = v instanceof BubbleTextView; + mFloatingView = new View(mLauncher); + if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) { + // Create a copy of the app icon + mFloatingView.setBackground( + DrawableFactory.get(mLauncher).newIcon((ItemInfoWithIcon) v.getTag())); + } + + // Position the floating view exactly on top of the original + Rect rect = new Rect(); + final boolean fromDeepShortcutView = v.getParent() instanceof DeepShortcutView; + if (fromDeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, rect); + } + int viewLocationLeft = rect.left; + int viewLocationTop = rect.top; + + float startScale = 1f; + if (isBubbleTextView && !fromDeepShortcutView) { + BubbleTextView btv = (BubbleTextView) v; + btv.getIconBounds(rect); + Drawable dr = btv.getIcon(); + if (dr instanceof FastBitmapDrawable) { + startScale = ((FastBitmapDrawable) dr).getAnimatedScale(); + } + } else { + rect.set(0, 0, rect.width(), rect.height()); + } + viewLocationLeft += rect.left; + viewLocationTop += rect.top; + int viewLocationStart = mIsRtl + ? windowTargetBounds.width() - rect.right + : viewLocationLeft; + LayoutParams lp = new LayoutParams(rect.width(), rect.height()); + lp.ignoreInsets = true; + lp.setMarginStart(viewLocationStart); + lp.topMargin = viewLocationTop; + mFloatingView.setLayoutParams(lp); + + // Set the properties here already to make sure they'are available when running the first + // animation frame. + mFloatingView.setLeft(viewLocationLeft); + mFloatingView.setTop(viewLocationTop); + mFloatingView.setRight(viewLocationLeft + rect.width()); + mFloatingView.setBottom(viewLocationTop + rect.height()); + + // Swap the two views in place. + ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView); + v.setVisibility(View.INVISIBLE); + + AnimatorSet appIconAnimatorSet = new AnimatorSet(); + int[] dragLayerBounds = new int[2]; + mDragLayer.getLocationOnScreen(dragLayerBounds); + + // Animate the app icon to the center of the window bounds in screen coordinates. + float centerX = windowTargetBounds.centerX() - dragLayerBounds[0]; + float centerY = windowTargetBounds.centerY() - dragLayerBounds[1]; + + float xPosition = mIsRtl + ? windowTargetBounds.width() - lp.getMarginStart() - rect.width() + : lp.getMarginStart(); + float dX = centerX - xPosition - (lp.width / 2); + float dY = centerY - lp.topMargin - (lp.height / 2); + + ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX); + ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY); + + // Use upward animation for apps that are either on the bottom half of the screen, or are + // relatively close to the center. + boolean useUpwardAnimation = lp.topMargin > centerY + || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx; + if (useUpwardAnimation) { + x.setDuration(APP_LAUNCH_CURVED_DURATION); + y.setDuration(APP_LAUNCH_DURATION); + } else { + x.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_DURATION)); + y.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_CURVED_DURATION)); + } + x.setInterpolator(AGGRESSIVE_EASE); + y.setInterpolator(AGGRESSIVE_EASE); + appIconAnimatorSet.play(x); + appIconAnimatorSet.play(y); + + // Scale the app icon to take up the entire screen. This simplifies the math when + // animating the app window position / scale. + float maxScaleX = windowTargetBounds.width() / (float) rect.width(); + float maxScaleY = windowTargetBounds.height() / (float) rect.height(); + float scale = Math.max(maxScaleX, maxScaleY); + ObjectAnimator scaleAnim = ObjectAnimator + .ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale); + scaleAnim.setDuration(APP_LAUNCH_DURATION) + .setInterpolator(Interpolators.EXAGGERATED_EASE); + appIconAnimatorSet.play(scaleAnim); + + // Fade out the app icon. + ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f); + if (useUpwardAnimation) { + alpha.setStartDelay(APP_LAUNCH_ALPHA_START_DELAY); + alpha.setDuration(APP_LAUNCH_ALPHA_DURATION); + } else { + alpha.setStartDelay((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR + * APP_LAUNCH_ALPHA_START_DELAY)); + alpha.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_ALPHA_DURATION)); + } + alpha.setInterpolator(LINEAR); + appIconAnimatorSet.play(alpha); + + appIconAnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Reset launcher to normal state + v.setVisibility(View.VISIBLE); + ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView); + } + }); + return appIconAnimatorSet; + } + + /** + * @return Animator that controls the window of the opening targets. + */ + private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets, + Rect windowTargetBounds) { + Rect bounds = new Rect(); + if (v.getParent() instanceof DeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), bounds); + } else if (v instanceof BubbleTextView) { + ((BubbleTextView) v).getIconBounds(bounds); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, bounds); + } + int[] floatingViewBounds = new int[2]; + + Rect crop = new Rect(); + Matrix matrix = new Matrix(); + + ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setDuration(APP_LAUNCH_DURATION); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + // Fade alpha for the app window. + FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR); + boolean isFirstFrame = true; + + @Override + public void onUpdate(float percent) { + final Surface surface = getSurface(mFloatingView); + final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1; + if (frameNumber == -1) { + // Booo, not cool! Our surface got destroyed, so no reason to animate anything. + Log.w(TAG, "Failed to animate, surface got destroyed."); + return; + } + final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent); + + // Calculate app icon size. + float iconWidth = bounds.width() * mFloatingView.getScaleX(); + float iconHeight = bounds.height() * mFloatingView.getScaleY(); + + // Scale the app window to match the icon size. + float scaleX = iconWidth / windowTargetBounds.width(); + float scaleY = iconHeight / windowTargetBounds.height(); + float scale = Math.min(1f, Math.min(scaleX, scaleY)); + matrix.setScale(scale, scale); + + // Position the scaled window on top of the icon + int windowWidth = windowTargetBounds.width(); + int windowHeight = windowTargetBounds.height(); + float scaledWindowWidth = windowWidth * scale; + float scaledWindowHeight = windowHeight * scale; + + float offsetX = (scaledWindowWidth - iconWidth) / 2; + float offsetY = (scaledWindowHeight - iconHeight) / 2; + mFloatingView.getLocationOnScreen(floatingViewBounds); + + float transX0 = floatingViewBounds[0] - offsetX; + float transY0 = floatingViewBounds[1] - offsetY; + matrix.postTranslate(transX0, transY0); + + // Animate the window crop so that it starts off as a square, and then reveals + // horizontally. + float cropHeight = windowHeight * easePercent + windowWidth * (1 - easePercent); + float initialTop = (windowHeight - windowWidth) / 2f; + crop.left = 0; + crop.top = (int) (initialTop * (1 - easePercent)); + crop.right = windowWidth; + crop.bottom = (int) (crop.top + cropHeight); + + TransactionCompat t = new TransactionCompat(); + if (isFirstFrame) { + RemoteAnimationProvider.prepareTargetsForFirstFrame(targets, t, MODE_OPENING); + isFirstFrame = false; + } + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + t.setAlpha(target.leash, mAlpha.value); + t.setMatrix(target.leash, matrix); + t.setWindowCrop(target.leash, crop); + t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface)); + } + } + t.setEarlyWakeup(); + t.apply(); + + matrix.reset(); + } + }); + return appAnimator; + } + + /** + * Registers remote animations used when closing apps to home screen. + */ + private void registerRemoteAnimations() { + // Unregister this + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); + definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, + WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, + new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(), + CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); + + // TODO: Transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER + new ActivityCompat(mLauncher).registerRemoteAnimations(definition); + } + } + + private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) { + return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode); + } + + /** + * @return Runner that plays when user goes to Launcher + * ie. pressing home, swiping up from nav bar. + */ + private RemoteAnimationRunnerCompat getWallpaperOpenRunner() { + return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) { + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + if (!mLauncher.hasBeenResumed()) { + // If launcher is not resumed, wait until new async-frame after resume + mLauncher.setOnResumeCallback(() -> + postAsyncCallback(mHandler, () -> + onCreateAnimation(targetCompats, result))); + return; + } + + AnimatorSet anim = null; + RemoteAnimationProvider provider = mRemoteAnimationProvider; + if (provider != null) { + anim = provider.createWindowAnimation(targetCompats); + } + + if (anim == null) { + anim = new AnimatorSet(); + anim.play(getClosingWindowAnimators(targetCompats)); + + // 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 (launcherIsATargetWithMode(targetCompats, MODE_OPENING) + || mLauncher.isForceInvisible()) { + // Only register the content animation for cancellation when state changes + mLauncher.getStateManager().setCurrentAnimation(anim); + createLauncherResumeAnimation(anim); + } + } + + mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); + result.setAnimation(anim); + } + }; + } + + /** + * Animator that controls the transformations of the windows the targets that are closing. + */ + private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) { + Matrix matrix = new Matrix(); + ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); + int duration = CLOSING_TRANSITION_DURATION_MS; + closingAnimator.setDuration(duration); + closingAnimator.addUpdateListener(new MultiValueUpdateListener() { + FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DEACCEL_1_7); + FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DEACCEL_1_7); + FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR); + + boolean isFirstFrame = true; + + @Override + public void onUpdate(float percent) { + TransactionCompat t = new TransactionCompat(); + if (isFirstFrame) { + RemoteAnimationProvider.prepareTargetsForFirstFrame(targets, t, MODE_CLOSING); + isFirstFrame = false; + } + for (RemoteAnimationTargetCompat app : targets) { + if (app.mode == RemoteAnimationTargetCompat.MODE_CLOSING) { + t.setAlpha(app.leash, mAlpha.value); + matrix.setScale(mScale.value, mScale.value, + app.sourceContainerBounds.centerX(), + app.sourceContainerBounds.centerY()); + matrix.postTranslate(0, mDy.value); + matrix.postTranslate(app.position.x, app.position.y); + t.setMatrix(app.leash, matrix); + } + } + t.setEarlyWakeup(); + t.apply(); + + matrix.reset(); + } + }); + + return closingAnimator; + } + + /** + * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}. + */ + private void createLauncherResumeAnimation(AnimatorSet anim) { + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + Pair contentAnimator = + getLauncherContentAnimator(false /* isAppOpening */); + contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY); + anim.play(contentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + contentAnimator.second.run(); + } + }); + } else { + AnimatorSet workspaceAnimator = new AnimatorSet(); + + mDragLayer.setTranslationY(-mWorkspaceTransY);; + workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, + -mWorkspaceTransY, 0)); + + mDragLayerAlpha.setValue(0); + workspaceAnimator.play(ObjectAnimator.ofFloat( + mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f)); + + workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); + workspaceAnimator.setDuration(333); + workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7); + + mDragLayer.getScrim().hideSysUiScrim(true); + + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + workspaceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetContentView(); + } + }); + anim.play(workspaceAnimator); + } + } + + private void resetContentView() { + mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd(); + mDragLayerAlpha.setValue(1f); + mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null); + mDragLayer.setTranslationY(0f); + mDragLayer.getScrim().hideSysUiScrim(false); + } + + private boolean hasControlRemoteAppTransitionPermission() { + return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java new file mode 100644 index 0000000000..08b6bfc6a0 --- /dev/null +++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java @@ -0,0 +1,91 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; + +import com.android.launcher3.states.InternalStateHandler; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.util.function.BiPredicate; + +@TargetApi(Build.VERSION_CODES.P) +public class LauncherInitListener extends InternalStateHandler implements ActivityInitListener { + + private final BiPredicate mOnInitListener; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + public LauncherInitListener(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + protected boolean init(Launcher launcher, boolean alreadyOnHome) { + if (mRemoteAnimationProvider != null) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) 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((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + RemoteAnimationProvider provider = mRemoteAnimationProvider; + mRemoteAnimationProvider = null; + + if (provider != null && launcher.getStateManager().getState().overviewUi) { + return provider.createWindowAnimation(targets); + } + return null; + }, cancellationSignal); + } + OverviewCallbacks.get(launcher).onInitOverviewTransition(); + return mOnInitListener.test(launcher, alreadyOnHome); + } + + @Override + public void register() { + initWhenReady(); + } + + @Override + public void unregister() { + mRemoteAnimationProvider = null; + clearReference(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + mRemoteAnimationProvider = animProvider; + + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(addToIntent(new Intent((intent))), options); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java new file mode 100644 index 0000000000..1eaa8bc92c --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides; + +import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; + +/** + * Definition for AllApps state + */ +public class AllAppsState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY; + + private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + + public AllAppsState(int id) { + super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, STATE_FLAGS); + } + + @Override + public void onStateEnabled(Launcher launcher) { + AbstractFloatingView.closeAllOpenViews(launcher); + dispatchWindowStateChanged(launcher); + } + + @Override + public String getDescription(Launcher launcher) { + AllAppsContainerView appsView = launcher.getAppsView(); + return appsView.getDescription(); + } + + @Override + public float getVerticalProgress(Launcher launcher) { + return 0f; + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + float[] scaleAndTranslation = LauncherState.OVERVIEW.getWorkspaceScaleAndTranslation( + launcher); + scaleAndTranslation[0] = 1; + return scaleAndTranslation; + } + + @Override + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return PAGE_ALPHA_PROVIDER; + } + + @Override + public int getVisibleElements(Launcher launcher) { + return ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {0.9f, -0.2f}; + } + + @Override + public LauncherState getHistoryForState(LauncherState previousState) { + return previousState == OVERVIEW ? OVERVIEW : NORMAL; + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java new file mode 100644 index 0000000000..2e6dcc0e77 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java @@ -0,0 +1,62 @@ +/* + * 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.uioverrides; + +import android.animation.ValueAnimator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.quickstep.OverviewInteractionState; + +public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler { + + private static final String TAG = "BackButtonAlphaHandler"; + + private final Launcher mLauncher; + private final OverviewInteractionState mOverviewInteractionState; + + public BackButtonAlphaHandler(Launcher launcher) { + mLauncher = launcher; + mOverviewInteractionState = OverviewInteractionState.getInstance(mLauncher); + } + + @Override + public void setState(LauncherState state) { + UiFactory.onLauncherStateOrFocusChanged(mLauncher); + } + + @Override + public void setStateWithAnimation(LauncherState toState, + AnimatorSetBuilder builder, LauncherStateManager.AnimationConfig config) { + if (!config.playNonAtomicComponent()) { + return; + } + float fromAlpha = mOverviewInteractionState.getBackButtonAlpha(); + float toAlpha = toState.hideBackButton ? 0 : 1; + if (Float.compare(fromAlpha, toAlpha) != 0) { + ValueAnimator anim = ValueAnimator.ofFloat(fromAlpha, toAlpha); + anim.setDuration(config.duration); + anim.addUpdateListener(valueAnimator -> { + final float alpha = (float) valueAnimator.getAnimatedValue(); + mOverviewInteractionState.setBackButtonAlpha(alpha, false); + }); + builder.play(anim); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java b/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java new file mode 100644 index 0000000000..2d9a161475 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java @@ -0,0 +1,48 @@ +/* + * 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.uioverrides; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.shared.system.RotationWatcher; + +/** + * Utility class for listening for rotation changes + */ +public class DisplayRotationListener extends RotationWatcher { + + private final Runnable mCallback; + private Handler mHandler; + + public DisplayRotationListener(Context context, Runnable callback) { + super(context); + mCallback = callback; + } + + @Override + public void enable() { + if (mHandler == null) { + mHandler = new Handler(); + } + super.enable(); + } + + @Override + protected void onRotationChanged(int i) { + mHandler.post(mCallback); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java new file mode 100644 index 0000000000..26453022f0 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java @@ -0,0 +1,85 @@ +/* + * 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.uioverrides; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.views.RecentsView; + +/** + * Extension of overview state used for QuickScrub + */ +public class FastOverviewState extends OverviewState { + + private static final float MAX_PREVIEW_SCALE_UP = 1.3f; + /** + * Vertical transition of the task previews relative to the full container. + */ + public static final float OVERVIEW_TRANSLATION_FACTOR = 0.4f; + + private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_DISABLE_INTERACTION + | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON | FLAG_DISABLE_ACCESSIBILITY; + + public FastOverviewState(int id) { + super(id, QuickScrubController.QUICK_SCRUB_FROM_HOME_START_DURATION, STATE_FLAGS); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + super.onStateTransitionEnd(launcher); + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + public int getVisibleElements(Launcher launcher) { + return NONE; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getTaskSize(sTempRect); + + return new float[] {getOverviewScale(launcher.getDeviceProfile(), sTempRect, launcher), + OVERVIEW_TRANSLATION_FACTOR}; + } + + public static float getOverviewScale(DeviceProfile dp, Rect taskRect, Context context) { + if (dp.isVerticalBarLayout()) { + return 1f; + } + + Resources res = context.getResources(); + float usedHeight = taskRect.height() + res.getDimension(R.dimen.task_thumbnail_top_margin); + float usedWidth = taskRect.width() + 2 * (res.getDimension(R.dimen.recents_page_spacing) + + res.getDimension(R.dimen.quickscrub_adjacent_visible_width)); + return Math.min(Math.min(dp.availableHeightPx / usedHeight, + dp.availableWidthPx / usedWidth), MAX_PREVIEW_SCALE_UP); + } + + @Override + public void onStateDisabled(Launcher launcher) { + super.onStateDisabled(launcher); + launcher.getOverviewPanel().getQuickScrubController().cancelActiveQuickscrub(); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java new file mode 100644 index 0000000000..6d1061990b --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java @@ -0,0 +1,79 @@ +package com.android.launcher3.uioverrides; + +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.quickstep.RecentsModel; + +/** + * Touch controller for handling edge swipes in landscape/seascape UI + */ +public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchController { + + private static final String TAG = "LandscapeEdgeSwipeCtrl"; + + public LandscapeEdgeSwipeController(Launcher l) { + super(l, SwipeDetector.HORIZONTAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + boolean draggingFromNav = mLauncher.getDeviceProfile().isSeascape() != isDragTowardPositive; + return draggingFromNav ? OVERVIEW : NORMAL; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.NAVBAR; + } + + @Override + protected float getShiftRange() { + return mLauncher.getDragLayer().getWidth(); + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponent) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState, + maxAccuracy, animComponent); + return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range; + } + + @Override + protected int getDirectionForLog() { + return mLauncher.getDeviceProfile().isSeascape() ? Direction.RIGHT : Direction.LEFT; + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java new file mode 100644 index 0000000000..9169ffbc1e --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides; + +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; +import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; + +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.views.RecentsView; + +/** + * Definition for overview state + */ +public class OverviewState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED + | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY; + + public OverviewState(int id) { + this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS); + } + + protected OverviewState(int id, int transitionDuration, int stateFlags) { + super(id, ContainerType.TASKSWITCHER, transitionDuration, stateFlags); + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + Workspace workspace = launcher.getWorkspace(); + View workspacePage = workspace.getPageAt(workspace.getCurrentPage()); + float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0 + ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx; + recentsView.getTaskSize(sTempRect); + float scale = (float) sTempRect.width() / workspacePageWidth; + float parallaxFactor = 0.5f; + return new float[]{scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor}; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {1f, 0f}; + } + + @Override + public void onStateEnabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(true); + AbstractFloatingView.closeAllOpenViews(launcher); + } + + @Override + public void onStateDisabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(false); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE); + DiscoveryBounce.showForOverviewIfNeeded(launcher); + } + + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + } + + @Override + public int getVisibleElements(Launcher launcher) { + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return VERTICAL_SWIPE_INDICATOR; + } else { + return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | + (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent() + ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS); + } + } + + @Override + public float getWorkspaceScrimAlpha(Launcher launcher) { + return 0.5f; + } + + @Override + public float getVerticalProgress(Launcher launcher) { + if ((getVisibleElements(launcher) & ALL_APPS_HEADER_EXTRA) == 0) { + // We have no all apps content, so we're still at the fully down progress. + return super.getVerticalProgress(launcher); + } + return 1 - (getDefaultSwipeHeight(launcher) + / launcher.getAllAppsController().getShiftRange()); + } + + public static float getDefaultSwipeHeight(Launcher launcher) { + DeviceProfile dp = launcher.getDeviceProfile(); + return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx; + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java new file mode 100644 index 0000000000..0f9b57f033 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; + +/** + * Touch controller from going from OVERVIEW to ALL_APPS. + * + * This is used in landscape mode. It is also used in portrait mode for the fallback recents. + */ +public class OverviewToAllAppsTouchController extends PortraitStatesTouchController { + + public OverviewToAllAppsTouchController(Launcher l) { + super(l); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + return mLauncher.getAppsView().shouldContainerScroll(ev); + } else if (mLauncher.isInState(NORMAL)) { + return true; + } else if (mLauncher.isInState(OVERVIEW)) { + RecentsView rv = mLauncher.getOverviewPanel(); + return ev.getY() > (rv.getBottom() - rv.getPaddingBottom()); + } else { + return false; + } + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (isDragTowardPositive) { + return ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.WORKSPACE; + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java new file mode 100644 index 0000000000..3fb7cd4802 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java @@ -0,0 +1,219 @@ +/* + * 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.uioverrides; + +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.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.MotionEvent; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +/** + * Touch controller for handling various state transitions in portrait UI. + */ +public class PortraitStatesTouchController extends AbstractStateChangeTouchController { + + private static final String TAG = "PortraitStatesTouchCtrl"; + + private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper(); + + // If true, we will finish the current animation instantly on second touch. + private boolean mFinishFastOnSecondTouch; + + + public PortraitStatesTouchController(Launcher l) { + super(l, SwipeDetector.VERTICAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + if (mFinishFastOnSecondTouch) { + // TODO: Animate to finish instead. + mCurrentAnimation.getAnimationPlayer().end(); + } + + // If we are already animating from a previous state, we can intercept. + return true; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + if (!mLauncher.getAppsView().shouldContainerScroll(ev)) { + return false; + } + } else { + // For all other states, only listen if the event originated below the hotseat height + DeviceProfile dp = mLauncher.getDeviceProfile(); + int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; + if (ev.getY() < (mLauncher.getDragLayer().getHeight() - hotseatHeight)) { + return false; + } + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return true; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (fromState == OVERVIEW) { + return isDragTowardPositive ? ALL_APPS : NORMAL; + } else if (fromState == NORMAL && isDragTowardPositive) { + return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return ContainerType.HOTSEAT; + } + + private AnimatorSetBuilder getNormalToOverviewAnimation() { + mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; + + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper); + + return builder; + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponents) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + + float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range; + float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range; + + float totalShift = endVerticalShift - startVerticalShift; + + final AnimatorSetBuilder builder; + + if (mFromState == NORMAL && mToState == OVERVIEW && totalShift != 0) { + builder = getNormalToOverviewAnimation(); + } else { + builder = new AnimatorSetBuilder(); + } + + cancelPendingAnim(); + + RecentsView recentsView = mLauncher.getOverviewPanel(); + TaskView taskView = (TaskView) recentsView.getChildAt(recentsView.getNextPage()); + if (recentsView.shouldSwipeDownLaunchApp() && mFromState == OVERVIEW && mToState == NORMAL + && taskView != null) { + mPendingAnimation = recentsView.createTaskLauncherAnimation(taskView, maxAccuracy); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + Runnable onCancelRunnable = () -> { + cancelPendingAnim(); + clearState(); + }; + mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy, + onCancelRunnable); + mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); + } else { + mCurrentAnimation = mLauncher.getStateManager() + .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState, + animComponents); + } + + if (totalShift == 0) { + totalShift = Math.signum(mFromState.ordinal - mToState.ordinal) + * OverviewState.getDefaultSwipeHeight(mLauncher); + } + return 1 / totalShift; + } + + private void cancelPendingAnim() { + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } + + @Override + protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, + LauncherState targetState, float velocity, boolean isFling) { + super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, + velocity, isFling); + handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); + } + + private void handleFirstSwipeToOverview(final ValueAnimator animator, + final long expectedDuration, final LauncherState targetState, final float velocity, + final boolean isFling) { + if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) { + mFinishFastOnSecondTouch = true; + if (isFling && expectedDuration != 0) { + // Update all apps interpolator to add a bit of overshoot starting from currFraction + final float currFraction = mCurrentAnimation.getProgressFraction(); + mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress( + new OvershootInterpolator(Math.min(Math.abs(velocity), 3f)), currFraction, 1); + animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION)) + .setInterpolator(LINEAR); + } + } else { + mFinishFastOnSecondTouch = false; + } + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } + + private static class InterpolatorWrapper implements Interpolator { + + public TimeInterpolator baseInterpolator = LINEAR; + + @Override + public float getInterpolation(float v) { + return baseInterpolator.getInterpolation(v); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java new file mode 100644 index 0000000000..ea27eb25b9 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides; + +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.RecentsViewContainer.CONTENT_ALPHA; + +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationConfig; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.PropertySetter; +import com.android.quickstep.views.LauncherRecentsView; +import com.android.quickstep.views.RecentsViewContainer; + +@TargetApi(Build.VERSION_CODES.O) +public class RecentsViewStateController implements StateHandler { + + private final Launcher mLauncher; + private final LauncherRecentsView mRecentsView; + private final RecentsViewContainer mRecentsViewContainer; + + public RecentsViewStateController(Launcher launcher) { + mLauncher = launcher; + mRecentsView = launcher.getOverviewPanel(); + mRecentsViewContainer = launcher.getOverviewPanelContainer(); + } + + @Override + public void setState(LauncherState state) { + mRecentsViewContainer.setContentAlpha(state.overviewUi ? 1 : 0); + float[] scaleTranslationYFactor = state.getOverviewScaleAndTranslationYFactor(mLauncher); + SCALE_PROPERTY.set(mRecentsView, scaleTranslationYFactor[0]); + mRecentsView.setTranslationYFactor(scaleTranslationYFactor[1]); + if (state.overviewUi) { + mRecentsView.updateEmptyMessage(); + mRecentsView.resetTaskVisuals(); + } + } + + @Override + public void setStateWithAnimation(final LauncherState toState, + AnimatorSetBuilder builder, AnimationConfig config) { + if (!config.playAtomicComponent()) { + // The entire recents animation is played atomically. + return; + } + PropertySetter setter = config.getPropertySetter(builder); + float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher); + Interpolator scaleAndTransYInterpolator = builder.getInterpolator( + ANIM_OVERVIEW_SCALE, LINEAR); + if (mLauncher.getStateManager().getState() == OVERVIEW && toState == FAST_OVERVIEW) { + scaleAndTransYInterpolator = Interpolators.clampToProgress( + QUICK_SCRUB_START_INTERPOLATOR, 0, QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleTranslationYFactor[0], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsViewContainer, CONTENT_ALPHA, toState.overviewUi ? 1 : 0, + builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); + + if (!toState.overviewUi) { + builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals); + } + + if (toState.overviewUi) { + ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1); + updateAnim.addUpdateListener(valueAnimator -> { + // While animating into recents, update the visible task data as needed + mRecentsView.loadVisibleTaskData(); + }); + updateAnim.setDuration(config.duration); + builder.play(updateAnim); + mRecentsView.updateEmptyMessage(); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java new file mode 100644 index 0000000000..a40573500f --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java @@ -0,0 +1,290 @@ +/* + * 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.uioverrides; + +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +/** + * Touch controller for handling task view card swipes + */ +public abstract class TaskViewTouchController + extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { + + private static final String TAG = "OverviewSwipeController"; + + // Progress after which the transition is assumed to be a success in case user does not fling + private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; + + protected final T mActivity; + private final SwipeDetector mDetector; + private final RecentsView mRecentsView; + private final int[] mTempCords = new int[2]; + + private PendingAnimation mPendingAnimation; + private AnimatorPlaybackController mCurrentAnimation; + private boolean mCurrentAnimationIsGoingUp; + + private boolean mNoIntercept; + + private float mDisplacementShift; + private float mProgressMultiplier; + private float mEndDisplacement; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + private TaskView mTaskBeingDragged; + + public TaskViewTouchController(T activity) { + mActivity = activity; + mRecentsView = activity.getOverviewPanel(); + mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL); + } + + private boolean canInterceptTouch() { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mActivity) != null) { + return false; + } + return isRecentsInteractive(); + } + + protected abstract boolean isRecentsInteractive(); + + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + } + + @Override + public void onAnimationCancel(Animator animation) { + if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { + clearState(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !canInterceptTouch(); + if (mNoIntercept) { + return false; + } + + // Now figure out which direction scroll events the controller will start + // calling the callbacks. + int directionsToDetectScroll = 0; + boolean ignoreSlopWhenSettling = false; + if (mCurrentAnimation != null) { + directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + ignoreSlopWhenSettling = true; + } else { + mTaskBeingDragged = null; + + for (int i = 0; i < mRecentsView.getChildCount(); i++) { + TaskView view = mRecentsView.getPageAt(i); + if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() + .isEventOverView(view, ev)) { + mTaskBeingDragged = view; + if (!OverviewInteractionState.getInstance(mActivity) + .isSwipeUpGestureEnabled()) { + // Don't allow swipe down to open if we don't support swipe up + // to enter overview. + directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + } else { + // The task can be dragged up to dismiss it, + // and down to open if it's the current page. + directionsToDetectScroll = i == mRecentsView.getCurrentPage() + ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; + } + break; + } + } + if (mTaskBeingDragged == null) { + mNoIntercept = true; + return false; + } + } + + mDetector.setDetectableScrollConditions( + directionsToDetectScroll, ignoreSlopWhenSettling); + } + + if (mNoIntercept) { + return false; + } + + onControllerTouchEvent(ev); + return mDetector.isDraggingOrSettling(); + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + private void reInitAnimationController(boolean goingUp) { + if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { + // No need to init + return; + } + int scrollDirections = mDetector.getScrollDirections(); + if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) + || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { + // Trying to re-init in an unsupported direction. + return; + } + if (mCurrentAnimation != null) { + mCurrentAnimation.setPlayFraction(0); + } + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + + mCurrentAnimationIsGoingUp = goingUp; + BaseDragLayer dl = mActivity.getDragLayer(); + long maxDuration = (long) (2 * dl.getHeight()); + + if (goingUp) { + mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, + true /* animateTaskView */, true /* removeTask */, maxDuration); + + mEndDisplacement = -mTaskBeingDragged.getHeight(); + } else { + mPendingAnimation = mRecentsView.createTaskLauncherAnimation( + mTaskBeingDragged, maxDuration); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + mTempCords[1] = mTaskBeingDragged.getHeight(); + dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords); + mEndDisplacement = dl.getHeight() - mTempCords[1]; + } + + if (mCurrentAnimation != null) { + mCurrentAnimation.setOnCancelRunnable(null); + } + mCurrentAnimation = AnimatorPlaybackController + .wrap(mPendingAnimation.anim, maxDuration, this::clearState); + onUserControlledAnimationCreated(mCurrentAnimation); + mCurrentAnimation.getTarget().addListener(this); + mCurrentAnimation.dispatchOnStart(); + mProgressMultiplier = 1 / mEndDisplacement; + } + + @Override + public void onDragStart(boolean start) { + if (mCurrentAnimation == null) { + reInitAnimationController(mDetector.wasInitialTouchPositive()); + mDisplacementShift = 0; + } else { + mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; + mCurrentAnimation.pause(); + } + mFlingBlockCheck.unblockFling(); + } + + @Override + public boolean onDrag(float displacement, float velocity) { + float totalDisplacement = displacement + mDisplacementShift; + boolean isGoingUp = + totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; + if (isGoingUp != mCurrentAnimationIsGoingUp) { + reInitAnimationController(isGoingUp); + mFlingBlockCheck.blockFling(); + } else { + mFlingBlockCheck.onEvent(); + } + mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); + return true; + } + + @Override + public void onDragEnd(float velocity, boolean fling) { + final boolean goingToEnd; + final int logAction; + boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + fling = false; + } + if (fling) { + logAction = Touch.FLING; + boolean goingUp = velocity < 0; + goingToEnd = goingUp == mCurrentAnimationIsGoingUp; + } else { + logAction = Touch.SWIPE; + goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS; + } + + float progress = mCurrentAnimation.getProgressFraction(); + long animationDuration = SwipeDetector.calculateDuration( + velocity, goingToEnd ? (1 - progress) : progress); + if (blockedFling && !goingToEnd) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); + } + + float nextFrameProgress = Utilities.boundToRange( + progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); + + mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction)); + + ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); + anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); + anim.setDuration(animationDuration); + anim.setInterpolator(scrollInterpolatorForVelocity(velocity)); + anim.start(); + } + + private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) { + if (mPendingAnimation != null) { + mPendingAnimation.finish(wasSuccess, logAction); + mPendingAnimation = null; + } + clearState(); + } + + private void clearState() { + mDetector.finishedScrolling(); + mDetector.setDetectableScrollConditions(0, false); + mTaskBeingDragged = null; + mCurrentAnimation = null; + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java new file mode 100644 index 0000000000..dd5dcbeaf8 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides; + +import static android.view.View.VISIBLE; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +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.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN; +import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.os.CancellationSignal; +import android.util.Base64; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppTransitionManagerImpl; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.RemoteFadeOutAnimationListener; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.zip.Deflater; + +public class UiFactory { + + public static TouchController[] createTouchControllers(Launcher launcher) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + if (!swipeUpEnabled) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LauncherTaskViewController(launcher)}; + } + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LandscapeEdgeSwipeController(launcher), + new LauncherTaskViewController(launcher)}; + } else { + return new TouchController[] { + launcher.getDragController(), + new PortraitStatesTouchController(launcher), + new LauncherTaskViewController(launcher)}; + } + } + + public static void setOnTouchControllersChangedListener(Context context, Runnable listener) { + OverviewInteractionState.getInstance(context).setOnSwipeUpSettingChangedListener(listener); + } + + public static StateHandler[] getStateHandler(Launcher launcher) { + return new StateHandler[] {launcher.getAllAppsController(), launcher.getWorkspace(), + new RecentsViewStateController(launcher), new BackButtonAlphaHandler(launcher)}; + } + + /** + * Sets the back button visibility based on the current state/window focus. + */ + public static void onLauncherStateOrFocusChanged(Launcher launcher) { + boolean shouldBackButtonBeHidden = launcher != null + && launcher.getStateManager().getState().hideBackButton + && launcher.hasWindowFocus(); + if (shouldBackButtonBeHidden) { + // Show the back button if there is a floating view visible. + shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher, + TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null; + } + OverviewInteractionState.getInstance(launcher) + .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */); + } + + public static void resetOverview(Launcher launcher) { + RecentsView recents = launcher.getOverviewPanel(); + recents.reset(); + } + + public static void onCreate(Launcher launcher) { + if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled + && finalState == ALL_APPS && prevState == NORMAL))) { + launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + + if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (finalState == ALL_APPS && prevState == OVERVIEW) { + launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + } + + public static void onStart(Context context) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onStart(); + } + } + + public static void onLauncherStateOrResumeChanged(Launcher launcher) { + LauncherState state = launcher.getStateManager().getState(); + DeviceProfile profile = launcher.getDeviceProfile(); + WindowManagerWrapper.getInstance().setShelfHeight( + state != ALL_APPS && launcher.isUserActive() && !profile.isVerticalBarLayout(), + profile.hotseatBarSizePx); + + if (state == NORMAL) { + launcher.getOverviewPanel().setSwipeDownShouldLaunchApp(false); + } + } + + public static void onTrimMemory(Context context, int level) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onTrimMemory(level); + } + } + + public static void useFadeOutAnimationForLauncherStart(Launcher launcher, + CancellationSignal cancellationSignal) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager(); + appTransitionManager.setRemoteAnimationProvider((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + + ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0); + fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(targets)); + AnimatorSet anim = new AnimatorSet(); + anim.play(fadeAnimation); + return anim; + }, cancellationSignal); + } + + public static boolean dumpActivity(Activity activity, PrintWriter writer) { + if (!Utilities.IS_DEBUG_DEVICE) { + return false; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) { + return false; + } + + Deflater deflater = new Deflater(); + deflater.setInput(out.toByteArray()); + deflater.finish(); + + out.reset(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); // returns the generated code... index + out.write(buffer, 0, count); + } + + writer.println("--encoded-view-dump-v0--"); + writer.println(Base64.encodeToString( + out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING)); + return true; + } + + public static void prepareToShowOverview(Launcher launcher) { + RecentsView overview = launcher.getOverviewPanel(); + if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) { + SCALE_PROPERTY.set(overview, 1.33f); + } + } + + private static class LauncherTaskViewController extends TaskViewTouchController { + + public LauncherTaskViewController(Launcher activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.isInState(OVERVIEW); + } + + @Override + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + mActivity.getStateManager().setCurrentUserControlledAnimation(animController); + } + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java new file mode 100644 index 0000000000..8218517dcc --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java @@ -0,0 +1,115 @@ +/* + * 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.uioverrides; + +import static android.app.WallpaperManager.FLAG_SYSTEM; + +import android.annotation.TargetApi; +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import com.android.systemui.shared.system.TonalCompat; +import com.android.systemui.shared.system.TonalCompat.ExtractionInfo; + +import java.util.ArrayList; + +@TargetApi(Build.VERSION_CODES.P) +public class WallpaperColorInfo implements OnColorsChangedListener { + + private static final Object sInstanceLock = new Object(); + private static WallpaperColorInfo sInstance; + + public static WallpaperColorInfo getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new WallpaperColorInfo(context.getApplicationContext()); + } + return sInstance; + } + } + + private final ArrayList mListeners = new ArrayList<>(); + private final WallpaperManager mWallpaperManager; + private final TonalCompat mTonalCompat; + + private ExtractionInfo mExtractionInfo; + + private OnChangeListener[] mTempListeners = new OnChangeListener[0]; + + private WallpaperColorInfo(Context context) { + mWallpaperManager = context.getSystemService(WallpaperManager.class); + mTonalCompat = new TonalCompat(context); + + mWallpaperManager.addOnColorsChangedListener(this, new Handler(Looper.getMainLooper())); + update(mWallpaperManager.getWallpaperColors(FLAG_SYSTEM)); + } + + public int getMainColor() { + return mExtractionInfo.mainColor; + } + + public int getSecondaryColor() { + return mExtractionInfo.secondaryColor; + } + + public boolean isDark() { + return mExtractionInfo.supportsDarkTheme; + } + + public boolean supportsDarkText() { + return mExtractionInfo.supportsDarkText; + } + + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if ((which & FLAG_SYSTEM) != 0) { + update(colors); + notifyChange(); + } + } + + private void update(WallpaperColors wallpaperColors) { + mExtractionInfo = mTonalCompat.extractDarkColors(wallpaperColors); + } + + public void addOnChangeListener(OnChangeListener listener) { + mListeners.add(listener); + } + + public void removeOnChangeListener(OnChangeListener listener) { + mListeners.remove(listener); + } + + private void notifyChange() { + // Create a new array to avoid concurrent modification when the activity destroys itself. + mTempListeners = mListeners.toArray(mTempListeners); + for (OnChangeListener listener : mTempListeners) { + if (listener != null) { + listener.onExtractedColorsChanged(this); + } + } + } + + public interface OnChangeListener { + void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo); + } +} diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java new file mode 100644 index 0000000000..52a6dd5fe5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherInitListener; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.uioverrides.FastOverviewState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.LauncherLayoutListener; +import com.android.quickstep.views.LauncherRecentsView; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +/** + * Utility class which abstracts out the logical differences between Launcher and RecentsActivity. + */ +@TargetApi(Build.VERSION_CODES.P) +public interface ActivityControlHelper { + + LayoutListener createLayoutListener(T activity); + + /** + * Updates the UI to indicate quick interaction. + */ + void onQuickInteractionStart(T activity, @Nullable RunningTaskInfo taskInfo, + boolean activityVisible); + + float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context); + + void executeOnWindowAvailable(T activity, Runnable action); + + void onTransitionCancelled(T activity, boolean activityVisible); + + int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect); + + void onSwipeUpComplete(T activity); + + AnimationFactory prepareRecentsUI(T activity, boolean activityVisible, + Consumer callback); + + ActivityInitListener createActivityInitListener(BiPredicate onInitListener); + + @Nullable + T getCreatedActivity(); + + @UiThread + @Nullable + RecentsView getVisibleRecentsView(); + + @UiThread + boolean switchToRecentsIfVisible(boolean fromRecentsButton); + + Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target); + + boolean shouldMinimizeSplitScreen(); + + /** + * @return {@code true} if recents activity should be started immediately on touchDown, + * {@code false} if it should deferred until some threshold is crossed. + */ + boolean deferStartingActivity(int downHitTarget); + + boolean supportsLongSwipe(T activity); + + AlphaProperty getAlphaProperty(T activity); + + /** + * Must return a non-null controller is supportsLongSwipe was true. + */ + LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet); + + /** + * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher} + */ + int getContainerType(); + + class LauncherActivityControllerHelper implements ActivityControlHelper { + + @Override + public LayoutListener createLayoutListener(Launcher activity) { + return new LauncherLayoutListener(activity); + } + + @Override + public void onQuickInteractionStart(Launcher activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + LauncherState fromState = activity.getStateManager().getState(); + activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible); + + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + controller.onQuickScrubStart(activityVisible && !fromState.overviewUi, this); + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + // The padding calculations are exactly same as that of RecentsView.setInsets + int topMargin = context.getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + int paddingTop = targetRect.rect.top - topMargin - dp.getInsets().top; + int paddingBottom = dp.availableHeightPx + dp.getInsets().top - targetRect.rect.bottom; + + return FastOverviewState.OVERVIEW_TRANSLATION_FACTOR * (paddingBottom - paddingTop); + } + + @Override + public void executeOnWindowAvailable(Launcher activity, Runnable action) { + activity.getWorkspace().runOnOverlayHidden(action); + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateLauncherTaskSize(context, dp, outRect.rect); + if (interactionType == INTERACTION_QUICK_SCRUB) { + outRect.scale = FastOverviewState.getOverviewScale(dp, outRect.rect, context); + } + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset; + } else { + return dp.heightPx - outRect.rect.bottom; + } + } + + @Override + public void onTransitionCancelled(Launcher activity, boolean activityVisible) { + LauncherState startState = activity.getStateManager().getRestState(); + activity.getStateManager().goToState(startState, activityVisible); + } + + @Override + public void onSwipeUpComplete(Launcher activity) { + // Re apply state in case we did something funky during the transition. + activity.getStateManager().reapplyState(); + DiscoveryBounce.showForOverviewIfNeeded(activity); + } + + @Override + public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible, + Consumer callback) { + final LauncherState startState = activity.getStateManager().getState(); + + LauncherState resetState = startState; + if (startState.disableRestore) { + resetState = activity.getStateManager().getRestState(); + } + activity.getStateManager().setRestState(resetState); + + if (!activityVisible) { + // Since the launcher is not visible, we can safely reset the scroll position. + // This ensures then the next swipe up to all-apps starts from scroll 0. + activity.getAppsView().reset(false /* animate */); + activity.getStateManager().goToState(OVERVIEW, false); + + // Optimization, hide the all apps view to prevent layout while initializing + activity.getAppsView().getContentView().setVisibility(View.GONE); + } + + return new AnimationFactory() { + @Override + public void createActivityController(long transitionLength, + @InteractionType int interactionType) { + createActivityControllerInternal(activity, activityVisible, startState, + transitionLength, interactionType, callback); + } + + @Override + public void onTransitionCancelled() { + activity.getStateManager().goToState(startState, false /* animate */); + } + }; + } + + private void createActivityControllerInternal(Launcher activity, boolean wasVisible, + LauncherState startState, long transitionLength, + @InteractionType int interactionType, + Consumer callback) { + LauncherState endState = interactionType == INTERACTION_QUICK_SCRUB + ? FAST_OVERVIEW : OVERVIEW; + if (wasVisible) { + DeviceProfile dp = activity.getDeviceProfile(); + long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); + activity.getStateManager().goToState(startState, false); + callback.accept(activity.getStateManager() + .createAnimationToNewWorkspace(endState, accuracy)); + return; + } + + if (activity.getDeviceProfile().isVerticalBarLayout()) { + return; + } + + AllAppsTransitionController controller = activity.getAllAppsController(); + AnimatorSet anim = new AnimatorSet(); + + float scrollRange = Math.max(controller.getShiftRange(), 1); + float progressDelta = (transitionLength / scrollRange); + + float endProgress = endState.getVerticalProgress(activity); + float startProgress = endProgress + progressDelta; + ObjectAnimator shiftAnim = ObjectAnimator.ofFloat( + controller, ALL_APPS_PROGRESS, startProgress, endProgress); + shiftAnim.setInterpolator(LINEAR); + anim.play(shiftAnim); + + anim.setDuration(transitionLength * 2); + activity.getStateManager().setCurrentAnimation(anim); + callback.accept(AnimatorPlaybackController.wrap(anim, transitionLength * 2)); + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new LauncherInitListener(onInitListener); + } + + @Nullable + @Override + public Launcher getCreatedActivity() { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + return null; + } + return (Launcher) app.getModel().getCallback(); + } + + @Nullable + @UiThread + private Launcher getVisibleLaucher() { + Launcher launcher = getCreatedActivity(); + return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ? + launcher : null; + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + Launcher launcher = getVisibleLaucher(); + return launcher != null && launcher.getStateManager().getState().overviewUi + ? launcher.getOverviewPanel() : null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + Launcher launcher = getVisibleLaucher(); + if (launcher != null) { + if (fromRecentsButton) { + launcher.getUserEventDispatcher().logActionCommand( + LauncherLogProto.Action.Command.RECENTS_BUTTON, + getContainerType(), + LauncherLogProto.ContainerType.TASKSWITCHER); + } + launcher.getStateManager().goToState(OVERVIEW); + return true; + } + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + return downHitTarget == HIT_TARGET_BACK || downHitTarget == HIT_TARGET_ROTATION; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + return homeBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + return true; + } + + @Override + public boolean supportsLongSwipe(Launcher activity) { + return !activity.getDeviceProfile().isVerticalBarLayout(); + } + + @Override + public LongSwipeHelper getLongSwipeController(Launcher activity, + RemoteAnimationTargetSet targetSet) { + if (activity.getDeviceProfile().isVerticalBarLayout()) { + return null; + } + return new LongSwipeHelper(activity, targetSet); + } + + @Override + public AlphaProperty getAlphaProperty(Launcher activity) { + return activity.getDragLayer().getAlphaProperty(DragLayer.ALPHA_INDEX_SWIPE_UP); + } + + @Override + public int getContainerType() { + final Launcher launcher = getVisibleLaucher(); + return launcher != null ? launcher.getStateManager().getState().containerType + : LauncherLogProto.ContainerType.APP; + } + } + + class FallbackActivityControllerHelper implements ActivityControlHelper { + + private final ComponentName mHomeComponent; + private final Handler mUiHandler = new Handler(Looper.getMainLooper()); + + public FallbackActivityControllerHelper(ComponentName homeComponent) { + mHomeComponent = homeComponent; + } + + @Override + public void onQuickInteractionStart(RecentsActivity activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + + // TODO: match user is as well + boolean startingFromHome = !activityVisible && + (taskInfo == null || Objects.equals(taskInfo.topActivity, mHomeComponent)); + controller.onQuickScrubStart(startingFromHome, this); + if (activityVisible) { + mUiHandler.postDelayed(controller::onFinishedTransitionToQuickScrub, + OVERVIEW_TRANSITION_MS); + } + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + return 0; + } + + @Override + public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) { + action.run(); + } + + @Override + public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) { + // TODO: + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateFallbackTaskSize(context, dp, outRect.rect); + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset; + } else { + return dp.heightPx - outRect.rect.bottom; + } + } + + @Override + public void onSwipeUpComplete(RecentsActivity activity) { + // TODO: + } + + @Override + public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible, + Consumer callback) { + if (activityVisible) { + return (transitionLength, interactionType) -> { }; + } + + RecentsViewContainer rv = activity.getOverviewPanelContainer(); + rv.setContentAlpha(0); + + return new AnimationFactory() { + + boolean isAnimatingHome = false; + + @Override + public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { + isAnimatingHome = targets != null && targets.isAnimatingHome(); + if (!isAnimatingHome) { + rv.setContentAlpha(1); + } + createActivityController(getSwipeUpDestinationAndLength( + activity.getDeviceProfile(), activity, INTERACTION_NORMAL, + new TransformedRect()), INTERACTION_NORMAL); + } + + @Override + public void createActivityController(long transitionLength, int interactionType) { + if (!isAnimatingHome) { + return; + } + + ObjectAnimator anim = ObjectAnimator + .ofFloat(rv, RecentsViewContainer.CONTENT_ALPHA, 0, 1); + anim.setDuration(transitionLength).setInterpolator(LINEAR); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(anim); + callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength)); + } + }; + } + + @Override + public LayoutListener createLayoutListener(RecentsActivity activity) { + // We do not change anything as part of layout changes in fallback activity. Return a + // default layout listener. + return new LayoutListener() { + @Override + public void open() { } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { } + + @Override + public void finish() { } + }; + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new RecentsActivityTracker(onInitListener); + } + + @Nullable + @Override + public RecentsActivity getCreatedActivity() { + return RecentsActivityTracker.getCurrentActivity(); + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + RecentsActivity activity = getCreatedActivity(); + if (activity != null && activity.hasWindowFocus()) { + return activity.getOverviewPanel(); + } + return null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + // Always defer starting the activity when using fallback + return true; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + // TODO: Remove this once b/77875376 is fixed + return target.sourceContainerBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + // TODO: Remove this once b/77875376 is fixed + return false; + } + + @Override + public boolean supportsLongSwipe(RecentsActivity activity) { + return false; + } + + @Override + public LongSwipeHelper getLongSwipeController(RecentsActivity activity, + RemoteAnimationTargetSet targetSet) { + return null; + } + + @Override + public AlphaProperty getAlphaProperty(RecentsActivity activity) { + return activity.getDragLayer().getAlphaProperty(0); + } + + @Override + public int getContainerType() { + return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER; + } + } + + interface LayoutListener { + + void open(); + + void setHandler(WindowTransformSwipeHandler handler); + + void finish(); + } + + interface ActivityInitListener { + + void register(); + + void unregister(); + + void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration); + } + + interface AnimationFactory { + + default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { } + + void createActivityController(long transitionLength, @InteractionType int interactionType); + + default void onTransitionCancelled() { } + } +} diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java new file mode 100644 index 0000000000..84dfdbd1dd --- /dev/null +++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.FloatProperty; + +/** + * A mutable float which allows animating the value + */ +public class AnimatedFloat { + + public static FloatProperty VALUE = new FloatProperty("value") { + @Override + public void setValue(AnimatedFloat obj, float v) { + obj.updateValue(v); + } + + @Override + public Float get(AnimatedFloat obj) { + return obj.value; + } + }; + + private final Runnable mUpdateCallback; + private ObjectAnimator mValueAnimator; + + public float value; + + public AnimatedFloat(Runnable updateCallback) { + mUpdateCallback = updateCallback; + } + + public ObjectAnimator animateToValue(float start, float end) { + cancelAnimation(); + mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end); + mValueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (mValueAnimator == animator) { + mValueAnimator = null; + } + } + }); + return mValueAnimator; + } + + /** + * Changes the value and calls the callback. + * Note that the value can be directly accessed as well to avoid notifying the callback. + */ + public void updateValue(float v) { + if (Float.compare(v, value) != 0) { + value = v; + mUpdateCallback.run(); + } + } + + public void cancelAnimation() { + if (mValueAnimator != null) { + mValueAnimator.cancel(); + } + } + + public void finishAnimation() { + if (mValueAnimator != null && mValueAnimator.isRunning()) { + mValueAnimator.end(); + } + } + + public ObjectAnimator getCurrentAnimation() { + return mValueAnimator; + } +} diff --git a/quickstep/src/com/android/quickstep/DeferredTouchConsumer.java b/quickstep/src/com/android/quickstep/DeferredTouchConsumer.java new file mode 100644 index 0000000000..8e83bd0792 --- /dev/null +++ b/quickstep/src/com/android/quickstep/DeferredTouchConsumer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; + +/** + * A TouchConsumer which defers all events on the UIThread until the consumer is created. + */ +@TargetApi(Build.VERSION_CODES.P) +public class DeferredTouchConsumer implements TouchConsumer { + + private final VelocityTracker mVelocityTracker; + private final DeferredTouchProvider mTouchProvider; + + private MotionEventQueue mMyQueue; + private TouchConsumer mTarget; + + public DeferredTouchConsumer(DeferredTouchProvider touchProvider) { + mVelocityTracker = VelocityTracker.obtain(); + mTouchProvider = touchProvider; + } + + @Override + public void accept(MotionEvent event) { + mTarget.accept(event); + } + + @Override + public void reset() { + mTarget.reset(); + } + + @Override + public void updateTouchTracking(int interactionType) { + mTarget.updateTouchTracking(interactionType); + } + + @Override + public void onQuickScrubEnd() { + mTarget.onQuickScrubEnd(); + } + + @Override + public void onQuickScrubProgress(float progress) { + mTarget.onQuickScrubProgress(progress); + } + + @Override + public void onQuickStep(MotionEvent ev) { + mTarget.onQuickStep(ev); + } + + @Override + public void onCommand(int command) { + mTarget.onCommand(command); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mMyQueue = queue; + return null; + } + + @Override + public void deferInit() { + mTarget = mTouchProvider.createTouchConsumer(mVelocityTracker); + mTarget.getIntrimChoreographer(mMyQueue); + } + + @Override + public boolean forceToLauncherConsumer() { + return mTarget.forceToLauncherConsumer(); + } + + @Override + public boolean deferNextEventToMainThread() { + // If our target is still null, defer the next target as well + TouchConsumer target = mTarget; + return target == null ? true : target.deferNextEventToMainThread(); + } + + @Override + public void onShowOverviewFromAltTab() { + mTarget.onShowOverviewFromAltTab(); + } + + public interface DeferredTouchProvider { + + TouchConsumer createTouchConsumer(VelocityTracker tracker); + } +} diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java new file mode 100644 index 0000000000..12757c0f79 --- /dev/null +++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.InstantAppInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.InstantAppResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of InstantAppResolver using platform APIs + */ +@SuppressWarnings("unused") +public class InstantAppResolverImpl extends InstantAppResolver { + + private static final String TAG = "InstantAppResolverImpl"; + public static final String COMPONENT_CLASS_MARKER = "@instantapp"; + + private final PackageManager mPM; + + public InstantAppResolverImpl(Context context) + throws NoSuchMethodException, ClassNotFoundException { + mPM = context.getPackageManager(); + } + + @Override + public boolean isInstantApp(ApplicationInfo info) { + return info.isInstantApp(); + } + + @Override + public boolean isInstantApp(AppInfo info) { + ComponentName cn = info.getTargetComponent(); + return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER); + } + + @Override + public List getInstantApps() { + try { + List result = new ArrayList<>(); + for (InstantAppInfo iai : mPM.getInstantApps()) { + ApplicationInfo info = iai.getApplicationInfo(); + if (info != null) { + result.add(info); + } + } + return result; + } catch (SecurityException se) { + Log.w(TAG, "getInstantApps failed. Launcher may not be the default home app.", se); + } catch (Exception e) { + Log.e(TAG, "Error calling API: getInstantApps", e); + } + return super.getInstantApps(); + } +} diff --git a/quickstep/src/com/android/quickstep/LauncherSearchIndexablesProvider.java b/quickstep/src/com/android/quickstep/LauncherSearchIndexablesProvider.java new file mode 100644 index 0000000000..f5e1f6ec56 --- /dev/null +++ b/quickstep/src/com/android/quickstep/LauncherSearchIndexablesProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Build; +import android.provider.SearchIndexablesContract.XmlResource; +import android.provider.SearchIndexablesProvider; +import android.util.Xml; + +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconShapeOverride; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; +import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; + +@TargetApi(Build.VERSION_CODES.O) +public class LauncherSearchIndexablesProvider extends SearchIndexablesProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryXmlResources(String[] strings) { + MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); + ResolveInfo settingsActivity = getContext().getPackageManager().resolveActivity( + new Intent(Intent.ACTION_APPLICATION_PREFERENCES) + .setPackage(getContext().getPackageName()), 0); + cursor.newRow() + .add(XmlResource.COLUMN_XML_RESID, R.xml.indexable_launcher_prefs) + .add(XmlResource.COLUMN_INTENT_ACTION, Intent.ACTION_APPLICATION_PREFERENCES) + .add(XmlResource.COLUMN_INTENT_TARGET_PACKAGE, getContext().getPackageName()) + .add(XmlResource.COLUMN_INTENT_TARGET_CLASS, settingsActivity.activityInfo.name); + return cursor; + } + + @Override + public Cursor queryRawData(String[] projection) { + return new MatrixCursor(INDEXABLES_RAW_COLUMNS); + } + + @Override + public Cursor queryNonIndexableKeys(String[] projection) { + MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); + if (!getContext().getSystemService(LauncherApps.class).hasShortcutHostPermission()) { + // We are not the current launcher. Hide all preferences + try (XmlResourceParser parser = getContext().getResources() + .getXml(R.xml.indexable_launcher_prefs)) { + final int depth = parser.getDepth(); + final int[] attrs = new int[] { android.R.attr.key }; + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.START_TAG) { + TypedArray a = getContext().obtainStyledAttributes( + Xml.asAttributeSet(parser), attrs); + cursor.addRow(new String[] {a.getString(0)}); + a.recycle(); + } + } + } catch (IOException |XmlPullParserException e) { + throw new RuntimeException(e); + } + } else if (!IconShapeOverride.isSupported(getContext())) { + cursor.addRow(new String[] {IconShapeOverride.KEY_PREFERENCE}); + } + return cursor; + } +} diff --git a/quickstep/src/com/android/quickstep/LongSwipeHelper.java b/quickstep/src/com/android/quickstep/LongSwipeHelper.java new file mode 100644 index 0000000000..fbcde8bba2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/LongSwipeHelper.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION; +import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber; +import static com.android.systemui.shared.recents.utilities.Utilities.getSurface; + +import android.animation.ValueAnimator; +import android.view.Surface; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +/** + * Utility class to handle long swipe from an app. + * This assumes the presence of Launcher activity as long swipe is not supported on the + * fallback activity. + */ +public class LongSwipeHelper { + + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS)); + + private final Launcher mLauncher; + private final RemoteAnimationTargetSet mTargetSet; + + private float mMaxSwipeDistance = 1; + private AnimatorPlaybackController mAnimator; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) { + mLauncher = launcher; + mTargetSet = targetSet; + init(); + } + + private void init() { + setTargetAlpha(0, true); + mFlingBlockCheck.blockFling(); + + // Init animations + AllAppsTransitionController controller = mLauncher.getAllAppsController(); + // TODO: Scale it down so that we can reach all-apps in screen space + mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange()); + mAnimator = mLauncher.getStateManager() + .createAnimationToNewWorkspace(ALL_APPS, Math.round(2 * mMaxSwipeDistance)); + mAnimator.dispatchOnStart(); + } + + public void onMove(float displacement) { + mAnimator.setPlayFraction(displacement / mMaxSwipeDistance); + mFlingBlockCheck.onEvent(); + } + + public void destroy() { + // TODO: We can probably also hide the task view + setTargetAlpha(1, false); + + mLauncher.getStateManager().goToState(OVERVIEW, false); + } + + public void end(float velocity, boolean isFling, Runnable callback) { + long duration = MAX_SWIPE_DURATION; + + final float currentFraction = mAnimator.getProgressFraction(); + final boolean toAllApps; + float endProgress; + + boolean blockedFling = isFling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + isFling = false; + } + + if (!isFling) { + toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS; + endProgress = toAllApps ? 1 : 0; + + long expectedDuration = Math.abs(Math.round((endProgress - currentFraction) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + } else { + toAllApps = velocity < 0; + endProgress = toAllApps ? 1 : 0; + + float minFlingVelocity = mLauncher.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) { + float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance; + + // 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(1000 * Math.abs(distanceToTravel / velocity)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + + if (blockedFling && !toAllApps) { + duration *= LauncherAnimUtils.blockedFlingDurationFactor(0); + } + final boolean finalIsFling = isFling; + mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, finalIsFling, callback)); + ValueAnimator animator = mAnimator.getAnimationPlayer(); + animator.setDuration(duration).setInterpolator(DEACCEL); + animator.setFloatValues(currentFraction, endProgress); + animator.start(); + } + + private void setTargetAlpha(float alpha, boolean defer) { + final Surface surface = getSurface(mLauncher.getDragLayer()); + final long frameNumber = defer && surface != null ? getNextFrameNumber(surface) : -1; + if (defer) { + if (frameNumber == -1) { + defer = false; + } else { + mLauncher.getDragLayer().invalidate(); + } + } + + TransactionCompat transaction = new TransactionCompat(); + for (RemoteAnimationTargetCompat app : mTargetSet.apps) { + if (!(app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { + transaction.setAlpha(app.leash, alpha); + if (defer) { + transaction.deferTransactionUntil(app.leash, surface, frameNumber); + } + } + } + transaction.setEarlyWakeup(); + transaction.apply(); + } + + private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) { + mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false); + if (!toAllApps) { + DiscoveryBounce.showForOverviewIfNeeded(mLauncher); + mLauncher.getOverviewPanel().setSwipeDownShouldLaunchApp(true); + } + + mLauncher.getUserEventDispatcher().logStateChangeAction( + isFling ? Touch.FLING : Touch.SWIPE, Direction.UP, + ContainerType.NAVBAR, ContainerType.APP, + toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER, + 0); + + callback.run(); + } +} diff --git a/quickstep/src/com/android/quickstep/MotionEventQueue.java b/quickstep/src/com/android/quickstep/MotionEventQueue.java new file mode 100644 index 0000000000..f73be6cba9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/MotionEventQueue.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_MASK; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; + +import android.annotation.TargetApi; +import android.os.Build; +import android.util.Log; +import android.view.Choreographer; +import android.view.MotionEvent; + +import com.android.systemui.shared.system.ChoreographerCompat; + +import java.util.ArrayList; + +/** + * Helper class for batching input events + */ +@TargetApi(Build.VERSION_CODES.O) +public class MotionEventQueue { + + private static final String TAG = "MotionEventQueue"; + + private static final int ACTION_VIRTUAL = ACTION_MASK - 1; + + private static final int ACTION_QUICK_SCRUB_START = + ACTION_VIRTUAL | (1 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_PROGRESS = + ACTION_VIRTUAL | (2 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_END = + ACTION_VIRTUAL | (3 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_RESET = + ACTION_VIRTUAL | (4 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_DEFER_INIT = + ACTION_VIRTUAL | (5 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_SHOW_OVERVIEW_FROM_ALT_TAB = + ACTION_VIRTUAL | (6 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_STEP = + ACTION_VIRTUAL | (7 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_COMMAND = + ACTION_VIRTUAL | (8 << ACTION_POINTER_INDEX_SHIFT); + + private final EventArray mEmptyArray = new EventArray(); + private final Object mExecutionLock = new Object(); + + // We use two arrays and swap the current index when one array is being consumed + private final EventArray[] mArrays = new EventArray[] {new EventArray(), new EventArray()}; + private int mCurrentIndex = 0; + + private final Runnable mMainFrameCallback = this::frameCallbackForMainChoreographer; + private final Runnable mInterimFrameCallback = this::frameCallbackForInterimChoreographer; + + private final Choreographer mMainChoreographer; + + private final TouchConsumer mConsumer; + + private Choreographer mInterimChoreographer; + private Choreographer mCurrentChoreographer; + + private Runnable mCurrentRunnable; + + public MotionEventQueue(Choreographer choreographer, TouchConsumer consumer) { + mMainChoreographer = choreographer; + mConsumer = consumer; + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + + setInterimChoreographer(consumer.getIntrimChoreographer(this)); + } + + public void setInterimChoreographer(Choreographer choreographer) { + synchronized (mExecutionLock) { + synchronized (mArrays) { + setInterimChoreographerLocked(choreographer); + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + } + } + + private void setInterimChoreographerLocked(Choreographer choreographer) { + mInterimChoreographer = choreographer; + if (choreographer == null) { + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + } else { + mCurrentChoreographer = mInterimChoreographer; + mCurrentRunnable = mInterimFrameCallback; + } + } + + public void queue(MotionEvent event) { + mConsumer.preProcessMotionEvent(event); + queueNoPreProcess(event); + } + + private void queueNoPreProcess(MotionEvent event) { + synchronized (mArrays) { + EventArray array = mArrays[mCurrentIndex]; + if (array.isEmpty()) { + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + + int eventAction = event.getAction(); + if (eventAction == ACTION_MOVE && array.lastEventAction == ACTION_MOVE) { + // Replace and recycle the last event + array.set(array.size() - 1, event).recycle(); + } else { + array.add(event); + array.lastEventAction = eventAction; + } + } + } + + private void frameCallbackForMainChoreographer() { + runFor(mMainChoreographer); + } + + private void frameCallbackForInterimChoreographer() { + runFor(mInterimChoreographer); + } + + private void runFor(Choreographer caller) { + synchronized (mExecutionLock) { + EventArray array = swapAndGetCurrentArray(caller); + int size = array.size(); + for (int i = 0; i < size; i++) { + MotionEvent event = array.get(i); + if (event.getActionMasked() == ACTION_VIRTUAL) { + switch (event.getAction()) { + case ACTION_QUICK_SCRUB_START: + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_SCRUB_PROGRESS: + mConsumer.onQuickScrubProgress(event.getX()); + break; + case ACTION_QUICK_SCRUB_END: + mConsumer.onQuickScrubEnd(); + break; + case ACTION_RESET: + mConsumer.reset(); + break; + case ACTION_DEFER_INIT: + mConsumer.deferInit(); + break; + case ACTION_SHOW_OVERVIEW_FROM_ALT_TAB: + mConsumer.onShowOverviewFromAltTab(); + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_STEP: + mConsumer.onQuickStep(event); + break; + case ACTION_COMMAND: + mConsumer.onCommand(event.getSource()); + break; + default: + Log.e(TAG, "Invalid virtual event: " + event.getAction()); + } + } else { + mConsumer.accept(event); + } + event.recycle(); + } + array.clear(); + array.lastEventAction = ACTION_CANCEL; + } + } + + private EventArray swapAndGetCurrentArray(Choreographer caller) { + synchronized (mArrays) { + if (caller != mCurrentChoreographer) { + return mEmptyArray; + } + EventArray current = mArrays[mCurrentIndex]; + mCurrentIndex = mCurrentIndex ^ 1; + return current; + } + } + + private void queueVirtualAction(int action, float progress) { + queueNoPreProcess(MotionEvent.obtain(0, 0, action, progress, 0, 0)); + } + + public void onQuickScrubStart() { + queueVirtualAction(ACTION_QUICK_SCRUB_START, 0); + } + + public void onOverviewShownFromAltTab() { + queueVirtualAction(ACTION_SHOW_OVERVIEW_FROM_ALT_TAB, 0); + } + + public void onQuickScrubProgress(float progress) { + queueVirtualAction(ACTION_QUICK_SCRUB_PROGRESS, progress); + } + + public void onQuickScrubEnd() { + queueVirtualAction(ACTION_QUICK_SCRUB_END, 0); + } + + public void onQuickStep(MotionEvent event) { + event.setAction(ACTION_QUICK_STEP); + queueNoPreProcess(event); + } + + public void reset() { + queueVirtualAction(ACTION_RESET, 0); + } + + public void deferInit() { + queueVirtualAction(ACTION_DEFER_INIT, 0); + } + + public void onCommand(int command) { + MotionEvent ev = MotionEvent.obtain(0, 0, ACTION_COMMAND, 0, 0, 0); + ev.setSource(command); + queueNoPreProcess(ev); + } + + public TouchConsumer getConsumer() { + return mConsumer; + } + + private static class EventArray extends ArrayList { + + public int lastEventAction = ACTION_CANCEL; + + public EventArray() { + super(4); + } + } +} diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java new file mode 100644 index 0000000000..bda3d06aac --- /dev/null +++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.util.SparseArray; + +/** + * Utility class to help manage multiple callbacks based on different states. + */ +public class MultiStateCallback { + + private final SparseArray mCallbacks = new SparseArray<>(); + + private int mState = 0; + + /** + * Adds the provided state flags to the global state and executes any callbacks as a result. + * @param stateFlag + */ + public void setState(int stateFlag) { + mState = mState | stateFlag; + + int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + int state = mCallbacks.keyAt(i); + + if ((mState & state) == state) { + Runnable callback = mCallbacks.valueAt(i); + if (callback != null) { + // Set the callback to null, so that it does not run again. + mCallbacks.setValueAt(i, null); + callback.run(); + } + } + } + } + + /** + * Sets the callbacks to be run when the provided states are enabled. + * The callback is only run once. + */ + public void addCallback(int stateMask, Runnable callback) { + mCallbacks.put(stateMask, callback); + } + + public int getState() { + return mState; + } + + public boolean hasStates(int stateMask) { + return (mState & stateMask) == stateMask; + } +} diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java new file mode 100644 index 0000000000..f875bb786e --- /dev/null +++ b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.TaskDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.UserHandle; +import android.util.LruCache; +import android.util.SparseArray; + +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.graphics.BitmapInfo; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.graphics.LauncherIcons; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; + +/** + * Extension of {@link IconLoader} with icon normalization support + */ +@TargetApi(Build.VERSION_CODES.O) +public class NormalizedIconLoader extends IconLoader { + + private final SparseArray mDefaultIcons = new SparseArray<>(); + private final DrawableFactory mDrawableFactory; + private LauncherIcons mLauncherIcons; + + public NormalizedIconLoader(Context context, TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + super(context, iconCache, activityInfoCache); + mDrawableFactory = DrawableFactory.get(context); + } + + @Override + public Drawable getDefaultIcon(int userId) { + synchronized (mDefaultIcons) { + BitmapInfo info = mDefaultIcons.get(userId); + if (info == null) { + info = getBitmapInfo(Resources.getSystem() + .getDrawable(android.R.drawable.sym_def_app_icon), userId, 0, false); + mDefaultIcons.put(userId, info); + } + + return new FastBitmapDrawable(info); + } + } + + @Override + protected Drawable createBadgedDrawable(Drawable drawable, int userId, TaskDescription desc) { + return new FastBitmapDrawable(getBitmapInfo(drawable, userId, desc.getPrimaryColor(), + false)); + } + + private synchronized BitmapInfo getBitmapInfo(Drawable drawable, int userId, + int primaryColor, boolean isInstantApp) { + if (mLauncherIcons == null) { + mLauncherIcons = LauncherIcons.obtain(mContext); + } + + mLauncherIcons.setWrapperBackgroundColor(primaryColor); + // User version code O, so that the icon is always wrapped in an adaptive icon container. + return mLauncherIcons.createBadgedIconBitmap(drawable, UserHandle.of(userId), + Build.VERSION_CODES.O, isInstantApp); + } + + @Override + protected Drawable getBadgedActivityIcon(ActivityInfo activityInfo, int userId, + TaskDescription desc) { + BitmapInfo bitmapInfo = getBitmapInfo( + activityInfo.loadUnbadgedIcon(mContext.getPackageManager()), + userId, + desc.getPrimaryColor(), + activityInfo.applicationInfo.isInstantApp()); + return mDrawableFactory.newIcon(bitmapInfo, activityInfo); + } +} diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java new file mode 100644 index 0000000000..c856282636 --- /dev/null +++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.MotionEvent.INVALID_POINTER_ID; + +import static com.android.systemui.shared.system.ActivityManagerWrapper + .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.SystemClock; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.Display; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.AssistDataReceiver; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.NavigationBarCompat; +import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RecentsAnimationListener; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Touch consumer for handling events originating from an activity other than Launcher + */ +@TargetApi(Build.VERSION_CODES.P) +public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer { + + private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150; + + private final SparseArray mAnimationStates = new SparseArray<>(); + private final RunningTaskInfo mRunningTask; + private final RecentsModel mRecentsModel; + private final Intent mHomeIntent; + private final ActivityControlHelper mActivityControlHelper; + private final MainThreadExecutor mMainThreadExecutor; + private final Choreographer mBackgroundThreadChoreographer; + private final OverviewCallbacks mOverviewCallbacks; + + private final boolean mIsDeferredDownTarget; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private int mActivePointerId = INVALID_POINTER_ID; + private boolean mPassedInitialSlop; + // Used for non-deferred gestures to determine when to start dragging + private int mQuickStepDragSlop; + private float mStartDisplacement; + private WindowTransformSwipeHandler mInteractionHandler; + private int mDisplayRotation; + private Rect mStableInsets = new Rect(); + + private VelocityTracker mVelocityTracker; + private MotionEventQueue mEventQueue; + private boolean mIsGoingToHome; + + public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo, + RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl, + MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer, + @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks, + VelocityTracker velocityTracker) { + super(base); + + mRunningTask = runningTaskInfo; + mRecentsModel = recentsModel; + mHomeIntent = homeIntent; + mVelocityTracker = velocityTracker; + mActivityControlHelper = activityControl; + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadChoreographer = backgroundThreadChoreographer; + mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget); + mOverviewCallbacks = overviewCallbacks; + } + + @Override + public void onShowOverviewFromAltTab() { + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + } + + @Override + public void accept(MotionEvent ev) { + if (mVelocityTracker == null) { + return; + } + switch (ev.getActionMasked()) { + case ACTION_DOWN: { + TraceHelper.beginSection("TouchInt"); + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mPassedInitialSlop = false; + mQuickStepDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); + + // Start the window animation on down to give more time for launcher to draw if the + // user didn't start the gesture over the back button + if (!mIsDeferredDownTarget) { + startTouchTrackingForWindowAnimation(ev.getEventTime()); + } + + Display display = getSystemService(WindowManager.class).getDefaultDisplay(); + mDisplayRotation = display.getRotation(); + WindowManagerWrapper.getInstance().getStableInsets(mStableInsets); + break; + } + case ACTION_POINTER_UP: { + int ptrIdx = ev.getActionIndex(); + int ptrId = ev.getPointerId(ptrIdx); + if (ptrId == mActivePointerId) { + final int newPointerIdx = ptrIdx == 0 ? 1 : 0; + mDownPos.set( + ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), + ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); + mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); + mActivePointerId = ev.getPointerId(newPointerIdx); + } + break; + } + case ACTION_MOVE: { + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + break; + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + float displacement = getDisplacement(ev); + if (!mPassedInitialSlop) { + if (!mIsDeferredDownTarget) { + // Normal gesture, ensure we pass the drag slop before we start tracking + // the gesture + if (Math.abs(displacement) > mQuickStepDragSlop) { + mPassedInitialSlop = true; + mStartDisplacement = displacement; + } + } + } + + if (mPassedInitialSlop && mInteractionHandler != null) { + // Move + mInteractionHandler.updateDisplacement(displacement - mStartDisplacement); + } + break; + } + case ACTION_CANCEL: + // TODO: Should be different than ACTION_UP + case ACTION_UP: { + TraceHelper.endSection("TouchInt"); + + finishTouchTracking(ev); + break; + } + } + } + + private void notifyGestureStarted() { + if (mInteractionHandler == null) { + return; + } + + mOverviewCallbacks.closeAllWindows(); + ActivityManagerWrapper.getInstance().closeSystemWindows( + CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + // Notify the handler that the gesture has actually started + mInteractionHandler.onGestureStarted(); + } + + private boolean isNavBarOnRight() { + return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0; + } + + private boolean isNavBarOnLeft() { + return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0; + } + + private void startTouchTrackingForWindowAnimation(long touchTimeMs) { + // Create the shared handler + RecentsAnimationState animationState = new RecentsAnimationState(); + final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler( + animationState.id, mRunningTask, this, touchTimeMs, mActivityControlHelper); + + // Preload the plan + mRecentsModel.loadTasks(mRunningTask.id, null); + mInteractionHandler = handler; + handler.setGestureEndCallback(mEventQueue::reset); + + CountDownLatch drawWaitLock = new CountDownLatch(1); + handler.setLauncherOnDrawCallback(() -> { + drawWaitLock.countDown(); + if (handler == mInteractionHandler) { + switchToMainChoreographer(); + } + }); + handler.initWhenReady(); + + TraceHelper.beginSection("RecentsController"); + Runnable startActivity = () -> ActivityManagerWrapper.getInstance().startRecentsActivity( + mHomeIntent, + new AssistDataReceiver() { + @Override + public void onHandleAssistData(Bundle bundle) { + mRecentsModel.preloadAssistData(mRunningTask.id, bundle); + } + }, animationState, null, null); + + if (Looper.myLooper() != Looper.getMainLooper()) { + startActivity.run(); + try { + drawWaitLock.await(LAUNCHER_DRAW_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // We have waited long enough for launcher to draw + } + } else { + // We should almost always get touch-town on background thread. This is an edge case + // when the background Choreographer has not yet initialized. + BackgroundExecutor.get().submit(startActivity); + } + } + + @Override + public void onCommand(int command) { + RecentsAnimationState state = mAnimationStates.get(command); + if (state != null) { + state.execute(); + } + } + + /** + * Called when the gesture has ended. Does not correlate to the completion of the interaction as + * the animation can still be running. + */ + private void finishTouchTracking(MotionEvent ev) { + if (mPassedInitialSlop && mInteractionHandler != null) { + mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); + + mVelocityTracker.computeCurrentVelocity(1000, + ViewConfiguration.get(this).getScaledMaximumFlingVelocity()); + + float velocity = isNavBarOnRight() ? mVelocityTracker.getXVelocity(mActivePointerId) + : isNavBarOnLeft() ? -mVelocityTracker.getXVelocity(mActivePointerId) + : mVelocityTracker.getYVelocity(mActivePointerId); + mInteractionHandler.onGestureEnded(velocity); + } else { + // Since we start touch tracking on DOWN, we may reach this state without actually + // starting the gesture. In that case, just cleanup immediately. + reset(); + + // Also clean up in case the system has handled the UP and canceled the animation before + // we had a chance to start the recents animation. In such a case, we will not receive + ActivityManagerWrapper.getInstance().cancelRecentsAnimation( + true /* restoreHomeStackPosition */); + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + @Override + public void reset() { + // Clean up the old interaction handler + if (mInteractionHandler != null) { + final WindowTransformSwipeHandler handler = mInteractionHandler; + mInteractionHandler = null; + mIsGoingToHome = handler.mIsGoingToHome; + mMainThreadExecutor.execute(handler::reset); + } + } + + @Override + public void updateTouchTracking(int interactionType) { + if (!mPassedInitialSlop && mIsDeferredDownTarget && mInteractionHandler == null) { + // If we deferred starting the window animation on touch down, then + // start tracking now + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + mPassedInitialSlop = true; + } + + if (mInteractionHandler != null) { + mInteractionHandler.updateInteractionType(interactionType); + } + notifyGestureStarted(); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mEventQueue = queue; + return mBackgroundThreadChoreographer; + } + + @Override + public void onQuickScrubEnd() { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubProgress(progress); + } + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mIsDeferredDownTarget) { + // Deferred gesture, start the animation and gesture tracking once we pass the actual + // touch slop + startTouchTrackingForWindowAnimation(ev.getEventTime()); + mPassedInitialSlop = true; + mStartDisplacement = getDisplacement(ev); + } + notifyGestureStarted(); + } + + private float getDisplacement(MotionEvent ev) { + float eventX = ev.getX(); + float eventY = ev.getY(); + float displacement = eventY - mDownPos.y; + if (isNavBarOnRight()) { + displacement = eventX - mDownPos.x; + } else if (isNavBarOnLeft()) { + displacement = mDownPos.x - eventX; + } + return displacement; + } + + public void switchToMainChoreographer() { + mEventQueue.setInterimChoreographer(null); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(ev); + if (ev.getActionMasked() == ACTION_POINTER_UP) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean forceToLauncherConsumer() { + return mIsGoingToHome; + } + + @Override + public boolean deferNextEventToMainThread() { + // TODO: Consider also check if the eventQueue is using mainThread of not. + return mInteractionHandler != null; + } + + private class RecentsAnimationState implements RecentsAnimationListener { + + private final int id; + + private RecentsAnimationControllerCompat mController; + private RemoteAnimationTargetSet mTargets; + private Rect mHomeContentInsets; + private Rect mMinimizedHomeBounds; + private boolean mCancelled; + + public RecentsAnimationState() { + id = mAnimationStates.size(); + mAnimationStates.put(id, this); + } + + @Override + public void onAnimationStart( + RecentsAnimationControllerCompat controller, + RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, + Rect minimizedHomeBounds) { + mController = controller; + mTargets = new RemoteAnimationTargetSet(apps, MODE_CLOSING); + mHomeContentInsets = homeContentInsets; + mMinimizedHomeBounds = minimizedHomeBounds; + mEventQueue.onCommand(id); + } + + @Override + public void onAnimationCanceled() { + mCancelled = true; + mEventQueue.onCommand(id); + } + + public void execute() { + if (mInteractionHandler == null || mInteractionHandler.id != id) { + if (!mCancelled && mController != null) { + TraceHelper.endSection("RecentsController", "Finishing no handler"); + mController.finish(false /* toHome */); + } + } else if (mCancelled) { + TraceHelper.endSection("RecentsController", + "Cancelled: " + mInteractionHandler); + mInteractionHandler.onRecentsAnimationCanceled(); + } else { + TraceHelper.partitionSection("RecentsController", "Received"); + mInteractionHandler.onRecentsAnimationStart(mController, mTargets, + mHomeContentInsets, mMinimizedHomeBounds); + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java new file mode 100644 index 0000000000..ac4a40b983 --- /dev/null +++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.content.Context; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; + +/** + * Callbacks related to overview/quicksteps. + */ +public class OverviewCallbacks { + + private static OverviewCallbacks sInstance; + + public static OverviewCallbacks get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(OverviewCallbacks.class, + context.getApplicationContext(), R.string.overview_callbacks_class); + } + return sInstance; + } + + public void onInitOverviewTransition() { } + + public void onResetOverview() { } + + public void closeAllWindows() { } +} diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java new file mode 100644 index 0000000000..41a45501d0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +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 com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.systemui.shared.system.ActivityManagerWrapper + .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.PackageManagerWrapper + .ACTION_PREFERRED_ACTIVITY_CHANGED; +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.AnimatorSet; +import android.animation.ValueAnimator; +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.pm.ResolveInfo; +import android.graphics.Rect; +import android.os.Build; +import android.os.PatternMatcher; +import android.os.SystemClock; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; +import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +import java.util.ArrayList; + +/** + * Helper class to handle various atomic commands for switching between Overview. + */ +@TargetApi(Build.VERSION_CODES.P) +public class OverviewCommandHelper { + + private static final long RECENTS_LAUNCH_DURATION = 250; + + private static final String TAG = "OverviewCommandHelper"; + + private final Context mContext; + private final ActivityManagerWrapper mAM; + private final RecentsModel mRecentsModel; + private final MainThreadExecutor mMainThreadExecutor; + private final ComponentName mMyHomeComponent; + + private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private String mUpdateRegisteredPackage; + + public Intent overviewIntent; + public ComponentName overviewComponent; + private ActivityControlHelper mActivityControlHelper; + + private long mLastToggleTime; + + public OverviewCommandHelper(Context context) { + mContext = context; + mAM = ActivityManagerWrapper.getInstance(); + mMainThreadExecutor = new MainThreadExecutor(); + mRecentsModel = RecentsModel.getInstance(mContext); + + Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(mContext.getPackageName()); + ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); + mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); + + mContext.registerReceiver(mUserPreferenceChangeReceiver, + new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); + initOverviewTargets(); + } + + private void initOverviewTargets() { + ComponentName defaultHome = PackageManagerWrapper.getInstance() + .getHomeActivities(new ArrayList<>()); + + final String overviewIntentCategory; + if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) { + // User default home is same as out home app. Use Overview integrated in Launcher. + overviewComponent = mMyHomeComponent; + mActivityControlHelper = new LauncherActivityControllerHelper(); + overviewIntentCategory = Intent.CATEGORY_HOME; + + if (mUpdateRegisteredPackage != null) { + // Remove any update listener as we don't care about other packages. + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } else { + // The default home app is a different launcher. Use the fallback Overview instead. + overviewComponent = new ComponentName(mContext, RecentsActivity.class); + mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome); + overviewIntentCategory = Intent.CATEGORY_DEFAULT; + + // User's default home app can change as a result of package updates of this app (such + // as uninstalling the app or removing the "Launcher" feature in an update). + // Listen for package updates of this app (and remove any previously attached + // package listener). + if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) { + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + } + + mUpdateRegisteredPackage = defaultHome.getPackageName(); + IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); + updateReceiver.addAction(ACTION_PACKAGE_CHANGED); + updateReceiver.addAction(ACTION_PACKAGE_REMOVED); + updateReceiver.addDataScheme("package"); + updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage, + PatternMatcher.PATTERN_LITERAL); + mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver); + } + } + + overviewIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(overviewIntentCategory) + .setComponent(overviewComponent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public void onDestroy() { + mContext.unregisterReceiver(mUserPreferenceChangeReceiver); + + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } + + public void onOverviewToggle() { + // If currently screen pinning, do not enter overview + if (mAM.isScreenPinningActive()) { + return; + } + + mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + mMainThreadExecutor.execute(new RecentsActivityCommand<>()); + } + + public void onOverviewShown() { + mMainThreadExecutor.execute(new ShowRecentsCommand()); + } + + public void onTip(int actionType, int viewType) { + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + UserEventDispatcher.newInstance(mContext, + new InvariantDeviceProfile(mContext).getDeviceProfile(mContext)) + .logActionTip(actionType, viewType); + } + }); + } + + public ActivityControlHelper getActivityControlHelper() { + return mActivityControlHelper; + } + + private class ShowRecentsCommand extends RecentsActivityCommand { + + @Override + protected boolean handleCommand(long elapsedTime) { + return mHelper.getVisibleRecentsView() != null; + } + } + + private class RecentsActivityCommand implements Runnable { + + protected final ActivityControlHelper mHelper; + private final long mCreateTime; + private final int mRunningTaskId; + + private ActivityInitListener mListener; + private T mActivity; + private RecentsView mRecentsView; + private final long mToggleClickedTime = SystemClock.uptimeMillis(); + private boolean mUserEventLogged; + + public RecentsActivityCommand() { + mHelper = getActivityControlHelper(); + mCreateTime = SystemClock.elapsedRealtime(); + mRunningTaskId = mAM.getRunningTask().id; + + // Preload the plan + mRecentsModel.loadTasks(mRunningTaskId, null); + } + + @Override + public void run() { + long elapsedTime = mCreateTime - mLastToggleTime; + mLastToggleTime = mCreateTime; + + if (!handleCommand(elapsedTime)) { + // Start overview + if (!mHelper.switchToRecentsIfVisible(true)) { + mListener = mHelper.createActivityInitListener(this::onActivityReady); + mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation, + mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION); + } + } + } + + protected boolean handleCommand(long elapsedTime) { + // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows + // the menu activity which takes window focus, preventing the right condition from + // being run below + RecentsView recents = mHelper.getVisibleRecentsView(); + if (recents != null) { + // Launch the next task + recents.showNextTask(); + return true; + } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) { + // The user tried to launch back into overview too quickly, either after + // launching an app, or before overview has actually shown, just ignore for now + return true; + } + return false; + } + + private boolean onActivityReady(T activity, Boolean wasVisible) { + activity.getOverviewPanel().setCurrentTask(mRunningTaskId); + AbstractFloatingView.closeAllOpenViews(activity, wasVisible); + AnimationFactory factory = mHelper.prepareRecentsUI(activity, wasVisible, + (controller) -> { + controller.dispatchOnStart(); + ValueAnimator anim = controller.getAnimationPlayer() + .setDuration(RECENTS_LAUNCH_DURATION); + anim.setInterpolator(FAST_OUT_SLOW_IN); + anim.start(); + }); + factory.onRemoteAnimationReceived(null); + if (wasVisible) { + factory.createActivityController(RECENTS_LAUNCH_DURATION, INTERACTION_NORMAL); + } + mActivity = activity; + mRecentsView = mActivity.getOverviewPanel(); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + if (!mUserEventLogged) { + activity.getUserEventDispatcher().logActionCommand(Action.Command.RECENTS_BUTTON, + mHelper.getContainerType(), ContainerType.TASKSWITCHER); + mUserEventLogged = true; + } + return false; + } + + private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) { + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents( + (int) (SystemClock.uptimeMillis() - mToggleClickedTime)); + } + + if (mListener != null) { + mListener.unregister(); + } + AnimatorSet anim = new AnimatorSet(); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + if (mRecentsView != null) { + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, + true /* animate */); + } + } + }); + if (mActivity == null) { + Log.e(TAG, "Animation created, before activity"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + RemoteAnimationTargetSet targetSet = + new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING); + + // Use the top closing app to determine the insets for the animation + RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); + if (runningTaskTarget == null) { + Log.e(TAG, "No closing app"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + final ClipAnimationHelper clipHelper = new ClipAnimationHelper(); + + // At this point, the activity is already started and laid-out. Get the home-bounds + // relative to the screen using the rootView of the activity. + int loc[] = new int[2]; + View rootView = mActivity.getRootView(); + rootView.getLocationOnScreen(loc); + Rect homeBounds = new Rect(loc[0], loc[1], + loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); + clipHelper.updateSource(homeBounds, runningTaskTarget); + + TransformedRect targetRect = new TransformedRect(); + mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, + INTERACTION_NORMAL, targetRect); + clipHelper.updateTargetRect(targetRect); + clipHelper.prepareAnimation(false /* isOpening */); + + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); + valueAnimator.setDuration(RECENTS_LAUNCH_DURATION); + valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + valueAnimator.addUpdateListener((v) -> + clipHelper.applyTransform(targetSet, (float) v.getAnimatedValue())); + + if (targetSet.isAnimatingHome()) { + // If we are animating home, fade in the opening targets + RemoteAnimationTargetSet openingSet = + new RemoteAnimationTargetSet(targetCompats, MODE_OPENING); + + TransactionCompat transaction = new TransactionCompat(); + valueAnimator.addUpdateListener((v) -> { + for (RemoteAnimationTargetCompat app : openingSet.apps) { + transaction.setAlpha(app.leash, (float) v.getAnimatedValue()); + } + transaction.apply(); + }); + } + anim.play(valueAnimator); + return anim; + } + } +} diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java new file mode 100644 index 0000000000..922a7ff295 --- /dev/null +++ b/quickstep/src/com/android/quickstep/OverviewInteractionState.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; +import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.util.UiThreadHelper; +import com.android.systemui.shared.recents.ISystemUiProxy; + +import java.util.concurrent.ExecutionException; + +/** + * Sets overview interaction flags, such as: + * + * - FLAG_DISABLE_QUICK_SCRUB + * - FLAG_DISABLE_SWIPE_UP + * - FLAG_SHOW_OVERVIEW_BUTTON + * + * @see com.android.systemui.shared.system.NavigationBarCompat.InteractionType and associated flags. + */ +public class OverviewInteractionState { + + private static final String TAG = "OverviewFlags"; + + private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once"; + private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME = + "config_swipe_up_gesture_setting_available"; + private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME = + "config_swipe_up_gesture_default"; + + // We do not need any synchronization for this variable as its only written on UI thread. + private static OverviewInteractionState INSTANCE; + + public static OverviewInteractionState getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new OverviewInteractionState(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> OverviewInteractionState.getInstance(context)).get(); + } catch (InterruptedException|ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private static final int MSG_SET_PROXY = 200; + private static final int MSG_SET_BACK_BUTTON_ALPHA = 201; + private static final int MSG_SET_SWIPE_UP_ENABLED = 202; + + private final SwipeUpGestureEnabledSettingObserver mSwipeUpSettingObserver; + + private final Context mContext; + private final Handler mUiHandler; + private final Handler mBgHandler; + + // These are updated on the background thread + private ISystemUiProxy mISystemUiProxy; + private boolean mSwipeUpEnabled = true; + private float mBackButtonAlpha = 1; + + private Runnable mOnSwipeUpSettingChangedListener; + + private OverviewInteractionState(Context context) { + mContext = context; + + // Data posted to the uihandler will be sent to the bghandler. Data is sent to uihandler + // because of its high send frequency and data may be very different than the previous value + // For example, send back alpha on uihandler to avoid flickering when setting its visibility + mUiHandler = new Handler(this::handleUiMessage); + mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage); + + if (getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME)) { + mSwipeUpSettingObserver = new SwipeUpGestureEnabledSettingObserver(mUiHandler, + context.getContentResolver()); + mSwipeUpSettingObserver.register(); + } else { + mSwipeUpSettingObserver = null; + mSwipeUpEnabled = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME); + } + } + + public boolean isSwipeUpGestureEnabled() { + return mSwipeUpEnabled; + } + + public float getBackButtonAlpha() { + return mBackButtonAlpha; + } + + public void setBackButtonAlpha(float alpha, boolean animate) { + if (!mSwipeUpEnabled) { + alpha = 1; + } + mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_ALPHA); + mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_ALPHA, animate ? 1 : 0, 0, alpha) + .sendToTarget(); + } + + public void setSystemUiProxy(ISystemUiProxy proxy) { + mBgHandler.obtainMessage(MSG_SET_PROXY, proxy).sendToTarget(); + } + + private boolean handleUiMessage(Message msg) { + if (msg.what == MSG_SET_BACK_BUTTON_ALPHA) { + mBackButtonAlpha = (float) msg.obj; + } + mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2, msg.obj).sendToTarget(); + return true; + } + + private boolean handleBgMessage(Message msg) { + switch (msg.what) { + case MSG_SET_PROXY: + mISystemUiProxy = (ISystemUiProxy) msg.obj; + break; + case MSG_SET_BACK_BUTTON_ALPHA: + applyBackButtonAlpha((float) msg.obj, msg.arg1 == 1); + return true; + case MSG_SET_SWIPE_UP_ENABLED: + mSwipeUpEnabled = msg.arg1 != 0; + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + + if (mOnSwipeUpSettingChangedListener != null) { + mOnSwipeUpSettingChangedListener.run(); + } + break; + } + applyFlags(); + return true; + } + + public void setOnSwipeUpSettingChangedListener(Runnable listener) { + mOnSwipeUpSettingChangedListener = listener; + } + + @WorkerThread + private void applyFlags() { + if (mISystemUiProxy == null) { + return; + } + + int flags = 0; + if (!mSwipeUpEnabled) { + flags = FLAG_DISABLE_SWIPE_UP | FLAG_DISABLE_QUICK_SCRUB | FLAG_SHOW_OVERVIEW_BUTTON; + } + try { + mISystemUiProxy.setInteractionState(flags); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview interaction flags", e); + } + } + + @WorkerThread + private void applyBackButtonAlpha(float alpha, boolean animate) { + if (mISystemUiProxy == null) { + return; + } + try { + mISystemUiProxy.setBackButtonAlpha(alpha, animate); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview back button alpha", e); + } + } + + private class SwipeUpGestureEnabledSettingObserver extends ContentObserver { + private Handler mHandler; + private ContentResolver mResolver; + private final int defaultValue; + + SwipeUpGestureEnabledSettingObserver(Handler handler, ContentResolver resolver) { + super(handler); + mHandler = handler; + mResolver = resolver; + defaultValue = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME) ? 1 : 0; + } + + public void register() { + mResolver.registerContentObserver(Settings.Secure.getUriFor(SWIPE_UP_SETTING_NAME), + false, this); + mSwipeUpEnabled = getValue(); + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED); + mHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED, getValue() ? 1 : 0, 0).sendToTarget(); + } + + private boolean getValue() { + return Settings.Secure.getInt(mResolver, SWIPE_UP_SETTING_NAME, defaultValue) == 1; + } + } + + private boolean getSystemBooleanRes(String resName) { + Resources res = Resources.getSystem(); + int resId = res.getIdentifier(resName, "bool", "android"); + + if (resId != 0) { + return res.getBoolean(resId); + } else { + Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); + return false; + } + } + + private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { + if (mSwipeUpEnabled && !Utilities.getPrefs(mContext).getBoolean( + HAS_ENABLED_QUICKSTEP_ONCE, true)) { + Utilities.getPrefs(mContext).edit() + .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true) + .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false) + .apply(); + } + } +} diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java new file mode 100644 index 0000000000..7a79c6f4df --- /dev/null +++ b/quickstep/src/com/android/quickstep/QuickScrubController.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.animation.Interpolator; + +import com.android.launcher3.Alarm; +import com.android.launcher3.BaseActivity; +import com.android.launcher3.OnAlarmListener; +import com.android.launcher3.Utilities; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +/** + * Responds to quick scrub callbacks to page through and launch recent tasks. + * + * The behavior is to evenly divide the progress into sections, each of which scrolls one page. + * The first and last section set an alarm to auto-advance backwards or forwards, respectively. + */ +public class QuickScrubController implements OnAlarmListener { + + public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; + public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 200; + // We want the translation y to finish faster than the rest of the animation. + public static final float QUICK_SCRUB_TRANSLATION_Y_FACTOR = 5f / 6; + public static final Interpolator QUICK_SCRUB_START_INTERPOLATOR = FAST_OUT_SLOW_IN; + + /** + * Snap to a new page when crossing these thresholds. The first and last auto-advance. + */ + private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] { + 0.05f, 0.20f, 0.35f, 0.50f, 0.65f, 0.80f, 0.95f + }; + + private static final String TAG = "QuickScrubController"; + private static final boolean ENABLE_AUTO_ADVANCE = true; + private static final long AUTO_ADVANCE_DELAY = 500; + private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325; + private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60; + + private final Alarm mAutoAdvanceAlarm; + private final RecentsView mRecentsView; + private final BaseActivity mActivity; + + private boolean mInQuickScrub; + private boolean mWaitingForTaskLaunch; + private int mQuickScrubSection; + private boolean mStartedFromHome; + private boolean mFinishedTransitionToQuickScrub; + private Runnable mOnFinishedTransitionToQuickScrubRunnable; + private ActivityControlHelper mActivityControlHelper; + + public QuickScrubController(BaseActivity activity, RecentsView recentsView) { + mActivity = activity; + mRecentsView = recentsView; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm = new Alarm(); + mAutoAdvanceAlarm.setOnAlarmListener(this); + } + } + + public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) { + prepareQuickScrub(TAG); + mInQuickScrub = true; + mStartedFromHome = startingFromHome; + mQuickScrubSection = 0; + mFinishedTransitionToQuickScrub = false; + mActivityControlHelper = controlHelper; + + snapToNextTaskIfAvailable(); + mActivity.getUserEventDispatcher().resetActionDurationMillis(); + } + + public void onQuickScrubEnd() { + mInQuickScrub = false; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.cancelAlarm(); + } + int page = mRecentsView.getNextPage(); + Runnable launchTaskRunnable = () -> { + TaskView taskView = mRecentsView.getPageAt(page); + if (taskView != null) { + mWaitingForTaskLaunch = true; + taskView.launchTask(true, (result) -> { + if (!result) { + taskView.notifyTaskLaunchFailed(TAG); + breakOutOfQuickScrub(); + } else { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(Touch.DRAGDROP, + LauncherLogProto.Action.Direction.NONE, page, + TaskUtils.getComponentKeyForTask(taskView.getTask().key)); + } + mWaitingForTaskLaunch = false; + }, taskView.getHandler()); + } else { + breakOutOfQuickScrub(); + } + mActivityControlHelper = null; + }; + int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) + * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; + if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { + // Settle on the page then launch it + mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable); + } else { + // No page move needed, just launch it + if (mFinishedTransitionToQuickScrub) { + launchTaskRunnable.run(); + } else { + mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable; + } + } + } + + public void cancelActiveQuickscrub() { + if (!mInQuickScrub) { + return; + } + Log.d(TAG, "Quickscrub was active, cancelling"); + mInQuickScrub = false; + mActivityControlHelper = null; + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + } + + /** + * Initializes the UI for quick scrub, returns true if success. + */ + public boolean prepareQuickScrub(String tag) { + if (mWaitingForTaskLaunch || mInQuickScrub) { + Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); + return false; + } + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + return true; + } + + public boolean isWaitingForTaskLaunch() { + return mWaitingForTaskLaunch; + } + + /** + * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction. + */ + private void breakOutOfQuickScrub() { + if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null + || !mActivityControlHelper.switchToRecentsIfVisible(false)) { + mActivity.onBackPressed(); + } + } + + public void onQuickScrubProgress(float progress) { + int quickScrubSection = 0; + for (float threshold : QUICK_SCRUB_THRESHOLDS) { + if (progress < threshold) { + break; + } + quickScrubSection++; + } + if (quickScrubSection != mQuickScrubSection) { + boolean cameFromAutoAdvance = mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + || mQuickScrubSection == 0; + int pageToGoTo = mRecentsView.getNextPage() + quickScrubSection - mQuickScrubSection; + if (mFinishedTransitionToQuickScrub && !cameFromAutoAdvance) { + goToPageWithHaptic(pageToGoTo); + } + if (ENABLE_AUTO_ADVANCE) { + if (quickScrubSection == QUICK_SCRUB_THRESHOLDS.length || quickScrubSection == 0) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } else { + mAutoAdvanceAlarm.cancelAlarm(); + } + } + mQuickScrubSection = quickScrubSection; + } + } + + public void onFinishedTransitionToQuickScrub() { + mFinishedTransitionToQuickScrub = true; + Runnable action = mOnFinishedTransitionToQuickScrubRunnable; + // Clear the runnable before executing it, to prevent potential recursion. + mOnFinishedTransitionToQuickScrubRunnable = null; + if (action != null) { + action.run(); + } + } + + public void snapToNextTaskIfAvailable() { + if (mInQuickScrub && mRecentsView.getChildCount() > 0) { + int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; + goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, + QUICK_SCRUB_START_INTERPOLATOR); + } + } + + private void goToPageWithHaptic(int pageToGoTo) { + goToPageWithHaptic(pageToGoTo, -1 /* overrideDuration */, false /* forceHaptic */, null); + } + + private void goToPageWithHaptic(int pageToGoTo, int overrideDuration, boolean forceHaptic, + Interpolator interpolator) { + pageToGoTo = Utilities.boundToRange(pageToGoTo, 0, mRecentsView.getPageCount() - 1); + boolean snappingToPage = pageToGoTo != mRecentsView.getNextPage(); + if (snappingToPage) { + int duration = overrideDuration > -1 ? overrideDuration + : Math.abs(pageToGoTo - mRecentsView.getNextPage()) + * QUICKSCRUB_SNAP_DURATION_PER_PAGE; + mRecentsView.snapToPage(pageToGoTo, duration, interpolator); + } + if (snappingToPage || forceHaptic) { + mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + } + + @Override + public void onAlarm(Alarm alarm) { + int currPage = mRecentsView.getNextPage(); + boolean recentsVisible = mActivityControlHelper != null + && mActivityControlHelper.getVisibleRecentsView() != null; + if (!recentsVisible) { + Log.w(TAG, "Failed to auto advance; recents not visible"); + return; + } + if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + && currPage < mRecentsView.getPageCount() - 1) { + goToPageWithHaptic(currPage + 1); + } else if (mQuickScrubSection == 0 && currPage > 0) { + goToPageWithHaptic(currPage - 1); + } + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } + } +} diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java new file mode 100644 index 0000000000..2c3f77f213 --- /dev/null +++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.content.Context; + +import com.android.launcher3.MainProcessInitializer; +import com.android.systemui.shared.system.ThreadedRendererCompat; + +@SuppressWarnings("unused") +public class QuickstepProcessInitializer extends MainProcessInitializer { + + public QuickstepProcessInitializer(Context context) { } + + @Override + protected void init(Context context) { + super.init(context); + + // Elevate GPU priority for Quickstep and Remote animations. + ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG); + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java new file mode 100644 index 0000000000..3adb290bb8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsActivity.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; + +import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAnimationRunner; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.badge.BadgeInfo; +import com.android.launcher3.uioverrides.UiFactory; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsRootView; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * A simple activity to show the recently launched tasks + */ +public class RecentsActivity extends BaseDraggingActivity { + + private Handler mUiHandler = new Handler(Looper.getMainLooper()); + private RecentsRootView mRecentsRootView; + private FallbackRecentsView mFallbackRecentsView; + private RecentsViewContainer mOverviewPanelContainer; + + private Configuration mOldConfig; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mOldConfig = new Configuration(getResources().getConfiguration()); + initDeviceProfile(); + + setContentView(R.layout.fallback_recents_activity); + mRecentsRootView = findViewById(R.id.drag_layer); + mFallbackRecentsView = findViewById(R.id.overview_panel); + mOverviewPanelContainer = findViewById(R.id.overview_panel_container); + + mRecentsRootView.setup(); + + getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW, + Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText)); + RecentsActivityTracker.onRecentsActivityCreate(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int diff = newConfig.diff(mOldConfig); + if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) { + onHandleConfigChanged(); + } + mOldConfig.setTo(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { + onHandleConfigChanged(); + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); + } + + public void onRootViewSizeChanged() { + if (isInMultiWindowModeCompat()) { + onHandleConfigChanged(); + } + } + + private void onHandleConfigChanged() { + mUserEventDispatcher = null; + initDeviceProfile(); + + AbstractFloatingView.closeOpenViews(this, true, + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + dispatchDeviceProfileChanged(); + + mRecentsRootView.setup(); + reapplyUi(); + } + + @Override + protected void reapplyUi() { + mRecentsRootView.dispatchInsets(); + } + + private void initDeviceProfile() { + // In case we are reusing IDP, create a copy so that we dont conflict with Launcher + // activity. + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + if (isInMultiWindowModeCompat()) { + InvariantDeviceProfile idp = appState == null + ? new InvariantDeviceProfile(this) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(this); + mDeviceProfile = mRecentsRootView == null ? dp.copy(this) + : dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize()); + } else { + // If we are reusing the Invariant device profile, make a copy. + mDeviceProfile = appState == null + ? new InvariantDeviceProfile(this).getDeviceProfile(this) + : appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this); + } + onDeviceProfileInitiated(); + } + + @Override + public BaseDragLayer getDragLayer() { + return mRecentsRootView; + } + + @Override + public View getRootView() { + return mRecentsRootView; + } + + @Override + public T getOverviewPanel() { + return (T) mFallbackRecentsView; + } + + public RecentsViewContainer getOverviewPanelContainer() { + return mOverviewPanelContainer; + } + + @Override + public BadgeInfo getBadgeInfoForItem(ItemInfo info) { + return null; + } + + @Override + public ActivityOptions getActivityLaunchOptions(final View v) { + if (!(v instanceof TaskView)) { + return null; + } + + final TaskView taskView = (TaskView) v; + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(composeRecentsLaunchAnimator(taskView, targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, RECENTS_LAUNCH_DURATION, + RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION)); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView, + RemoteAnimationTargetCompat[] targets) { + AnimatorSet target = new AnimatorSet(); + boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING); + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper) + .setDuration(RECENTS_LAUNCH_DURATION)); + + // Found a visible recents task that matches the opening app, lets launch the app from there + if (activityClosing) { + Animator adjacentAnimation = mFallbackRecentsView + .createAdjacentPageAnimForTaskLaunch(taskView, helper); + adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION); + adjacentAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mFallbackRecentsView.resetTaskVisuals(); + } + }); + target.play(adjacentAnimation); + } + return target; + } + + @Override + public void invalidateParent(ItemInfo info) { } + + @Override + protected void onStart() { + // Set the alpha to 1 before calling super, as it may get set back to 0 due to + // onActivityStart callback. + mFallbackRecentsView.setContentAlpha(1); + super.onStart(); + UiFactory.onStart(this); + mFallbackRecentsView.resetTaskVisuals(); + } + + @Override + protected void onStop() { + super.onStop(); + + // Workaround for b/78520668, explicitly trim memory once UI is hidden + onTrimMemory(TRIM_MEMORY_UI_HIDDEN); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + UiFactory.onTrimMemory(this, level); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + RecentsActivityTracker.onRecentsActivityNewIntent(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + RecentsActivityTracker.onRecentsActivityDestroy(this); + } + + @Override + public void onBackPressed() { + // TODO: Launch the task we came from + startHome(); + } + + public void startHome() { + startActivity(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(prefix + "Misc:"); + dumpMisc(writer); + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java new file mode 100644 index 0000000000..fb6090e2a5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + +import com.android.launcher3.MainThreadExecutor; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.lang.ref.WeakReference; +import java.util.function.BiPredicate; + +/** + * Utility class to track create/destroy for RecentsActivity + */ +@TargetApi(Build.VERSION_CODES.P) +public class RecentsActivityTracker implements ActivityInitListener { + + private static WeakReference sCurrentActivity = new WeakReference<>(null); + private static final Scheduler sScheduler = new Scheduler(); + + private final BiPredicate mOnInitListener; + + public RecentsActivityTracker(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + public void register() { + sScheduler.schedule(this); + } + + @Override + public void unregister() { + sScheduler.clearReference(this); + } + + private boolean init(RecentsActivity activity, boolean visible) { + return mOnInitListener.test(activity, visible); + } + + public static RecentsActivity getCurrentActivity() { + return sCurrentActivity.get(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(intent, options); + } + + public static void onRecentsActivityCreate(RecentsActivity activity) { + sCurrentActivity = new WeakReference<>(activity); + sScheduler.initIfPending(activity, false); + } + + + public static void onRecentsActivityNewIntent(RecentsActivity activity) { + sScheduler.initIfPending(activity, activity.isStarted()); + } + + public static void onRecentsActivityDestroy(RecentsActivity activity) { + if (sCurrentActivity.get() == activity) { + sCurrentActivity.clear(); + } + } + + + private static class Scheduler implements Runnable { + + private WeakReference mPendingTracker = new WeakReference<>(null); + private MainThreadExecutor mMainThreadExecutor; + + public synchronized void schedule(RecentsActivityTracker tracker) { + mPendingTracker = new WeakReference<>(tracker); + if (mMainThreadExecutor == null) { + mMainThreadExecutor = new MainThreadExecutor(); + } + mMainThreadExecutor.execute(this); + } + + @Override + public void run() { + RecentsActivity activity = sCurrentActivity.get(); + if (activity != null) { + initIfPending(activity, activity.isStarted()); + } + } + + public synchronized boolean initIfPending(RecentsActivity activity, boolean alreadyOnHome) { + RecentsActivityTracker tracker = mPendingTracker.get(); + if (tracker != null) { + if (!tracker.init(activity, alreadyOnHome)) { + mPendingTracker.clear(); + } + return true; + } + return false; + } + + public synchronized boolean clearReference(RecentsActivityTracker tracker) { + if (mPendingTracker.get() == tracker) { + mPendingTracker.clear(); + return true; + } + return false; + } + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java new file mode 100644 index 0000000000..34d42ac927 --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import com.android.launcher3.util.LooperExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.util.UiThreadHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; + +/** + * Wrapper around RecentsAnimationController to help with some synchronization + */ +public class RecentsAnimationWrapper { + + // A list of callbacks to run when we receive the recents animation target. There are different + // than the state callbacks as these run on the current worker thread. + private final ArrayList mCallbacks = new ArrayList<>(); + + public RemoteAnimationTargetSet targetSet; + + private RecentsAnimationControllerCompat mController; + private boolean mInputConsumerEnabled = false; + private boolean mBehindSystemBars = true; + private boolean mSplitScreenMinimized = false; + + private final ExecutorService mExecutorService = + new LooperExecutor(UiThreadHelper.getBackgroundLooper()); + + public synchronized void setController( + RecentsAnimationControllerCompat controller, RemoteAnimationTargetSet targetSet) { + TraceHelper.partitionSection("RecentsController", "Set controller " + controller); + this.mController = controller; + this.targetSet = targetSet; + + if (mInputConsumerEnabled) { + enableInputConsumer(); + } + + if (!mCallbacks.isEmpty()) { + for (Runnable action : new ArrayList<>(mCallbacks)) { + action.run(); + } + mCallbacks.clear(); + } + } + + public synchronized void runOnInit(Runnable action) { + if (targetSet == null) { + mCallbacks.add(action); + } else { + action.run(); + } + } + + /** + * @param onFinishComplete A callback that runs after the animation controller has finished + * on the background thread. + */ + public void finish(boolean toHome, Runnable onFinishComplete) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + mController = null; + TraceHelper.endSection("RecentsController", + "Finish " + controller + ", toHome=" + toHome); + if (controller != null) { + controller.setInputConsumerEnabled(false); + controller.finish(toHome); + if (onFinishComplete != null) { + onFinishComplete.run(); + } + } + }); + } + + public void enableInputConsumer() { + mInputConsumerEnabled = true; + if (mInputConsumerEnabled) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Enabling consumer on " + controller); + if (controller != null) { + controller.setInputConsumerEnabled(true); + } + }); + } + } + + public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { + if (mBehindSystemBars == behindSystemBars) { + return; + } + mBehindSystemBars = behindSystemBars; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting behind system bars on " + controller); + if (controller != null) { + controller.setAnimationTargetsBehindSystemBars(behindSystemBars); + } + }); + } + + /** + * NOTE: As a workaround for conflicting animations (Launcher animating the task leash, and + * SystemUI resizing the docked stack, which resizes the task), we currently only set the + * minimized mode, and not the inverse. + * TODO: Synchronize the minimize animation with the launcher animation + */ + public void setSplitScreenMinimizedForTransaction(boolean minimized) { + if (mSplitScreenMinimized || !minimized) { + return; + } + mSplitScreenMinimized = minimized; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting minimize dock on " + controller); + if (controller != null) { + controller.setSplitScreenMinimized(minimized); + } + }); + } + + public void hideCurrentInputMethod() { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Hiding currentinput method on " + controller); + if (controller != null) { + controller.hideCurrentInputMethod(); + } + }); + } + + public RecentsAnimationControllerCompat getController() { + return mController; + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java new file mode 100644 index 0000000000..9c2c8b3130 --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.util.LruCache; +import android.util.SparseArray; +import android.view.accessibility.AccessibilityManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +/** + * Singleton class to load and manage recents model. + */ +@TargetApi(Build.VERSION_CODES.O) +public class RecentsModel extends TaskStackChangeListener { + // We do not need any synchronization for this variable as its only written on UI thread. + private static RecentsModel INSTANCE; + + public static RecentsModel getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new RecentsModel(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> RecentsModel.getInstance(context)).get(); + } catch (InterruptedException|ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private final SparseArray mCachedAssistData = new SparseArray<>(1); + private final ArrayList mAssistDataListeners = new ArrayList<>(); + + private final Context mContext; + private final RecentsTaskLoader mRecentsTaskLoader; + private final MainThreadExecutor mMainThreadExecutor; + + private RecentsTaskLoadPlan mLastLoadPlan; + private int mLastLoadPlanId; + private int mTaskChangeId; + private ISystemUiProxy mSystemUiProxy; + private boolean mClearAssistCacheOnStackChange = true; + private final boolean mIsLowRamDevice; + private boolean mPreloadTasksInBackground; + private final AccessibilityManager mAccessibilityManager; + + private RecentsModel(Context context) { + mContext = context; + + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mIsLowRamDevice = activityManager.isLowRamDevice(); + mMainThreadExecutor = new MainThreadExecutor(); + + Resources res = context.getResources(); + mRecentsTaskLoader = new RecentsTaskLoader(mContext, + res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize), + res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0) { + + @Override + protected IconLoader createNewIconLoader(Context context, + TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + return new NormalizedIconLoader(context, iconCache, activityInfoCache); + } + }; + mRecentsTaskLoader.startLoader(mContext); + ActivityManagerWrapper.getInstance().registerTaskStackListener(this); + + mTaskChangeId = 1; + loadTasks(-1, null); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + } + + public RecentsTaskLoader getRecentsTaskLoader() { + return mRecentsTaskLoader; + } + + /** + * Preloads the task plan + * @param taskId The running task id or -1 + * @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 loadTasks(int taskId, Consumer callback) { + final int requestId = mTaskChangeId; + + // Fail fast if nothing has changed. + if (mLastLoadPlanId == mTaskChangeId) { + if (callback != null) { + final RecentsTaskLoadPlan plan = mLastLoadPlan; + mMainThreadExecutor.execute(() -> callback.accept(plan)); + } + return requestId; + } + + BackgroundExecutor.get().submit(() -> { + // Preload the plan + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext); + PreloadOptions opts = new PreloadOptions(); + opts.loadTitles = mAccessibilityManager.isEnabled(); + loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId()); + // Set the load plan on UI thread + mMainThreadExecutor.execute(() -> { + mLastLoadPlan = loadPlan; + mLastLoadPlanId = requestId; + + if (callback != null) { + callback.accept(loadPlan); + } + }); + }); + return requestId; + } + + public void setPreloadTasksInBackground(boolean preloadTasksInBackground) { + mPreloadTasksInBackground = preloadTasksInBackground && !mIsLowRamDevice; + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + mTaskChangeId++; + } + + @Override + public void onActivityUnpinned() { + mTaskChangeId++; + } + + @Override + public void onTaskStackChanged() { + mTaskChangeId++; + + Preconditions.assertUIThread(); + if (mClearAssistCacheOnStackChange) { + mCachedAssistData.clear(); + } else { + mClearAssistCacheOnStackChange = true; + } + } + + @Override + public void onTaskStackChangedBackground() { + int userId = UserHandle.myUserId(); + if (!mPreloadTasksInBackground || !checkCurrentOrManagedUserId(userId, mContext)) { + // TODO: Only register this for the current user + return; + } + + // Preload a fixed number of task icons/thumbnails in the background + ActivityManager.RunningTaskInfo runningTaskInfo = + ActivityManagerWrapper.getInstance().getRunningTask(); + RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext); + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1; + launchOpts.numVisibleTasks = 2; + launchOpts.numVisibleTaskThumbnails = 2; + launchOpts.onlyLoadForCache = true; + launchOpts.onlyLoadPausedActivities = true; + launchOpts.loadThumbnails = true; + PreloadOptions preloadOpts = new PreloadOptions(); + preloadOpts.loadTitles = mAccessibilityManager.isEnabled(); + plan.preloadPlan(preloadOpts, mRecentsTaskLoader, -1, userId); + mRecentsTaskLoader.loadTasks(plan, launchOpts); + } + + public boolean isLoadPlanValid(int resultId) { + return mTaskChangeId == resultId; + } + + public RecentsTaskLoadPlan getLastLoadPlan() { + return mLastLoadPlan; + } + + public void setSystemUiProxy(ISystemUiProxy systemUiProxy) { + mSystemUiProxy = systemUiProxy; + } + + public ISystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + + public void onStart() { + mRecentsTaskLoader.startLoader(mContext); + mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(true); + } + + public void onTrimMemory(int level) { + if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + // We already stop the loader in UI_HIDDEN, so stop the high res loader as well + mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(false); + } + mRecentsTaskLoader.onTrimMemory(level); + } + + public void onOverviewShown(boolean fromHome, String tag) { + if (mSystemUiProxy == null) { + return; + } + try { + mSystemUiProxy.onOverviewShown(fromHome); + } catch (RemoteException e) { + Log.w(tag, + "Failed to notify SysUI of overview shown from " + (fromHome ? "home" : "app") + + ": ", e); + } + } + + @WorkerThread + public void preloadAssistData(int taskId, Bundle data) { + mMainThreadExecutor.execute(() -> { + mCachedAssistData.put(taskId, data); + // We expect a stack change callback after the assist data is set. So ignore the + // very next stack change callback. + mClearAssistCacheOnStackChange = false; + + int count = mAssistDataListeners.size(); + for (int i = 0; i < count; i++) { + mAssistDataListeners.get(i).onAssistDataReceived(taskId); + } + }); + } + + public Bundle getAssistData(int taskId) { + Preconditions.assertUIThread(); + return mCachedAssistData.get(taskId); + } + + public void addAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.add(listener); + } + + public void removeAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.remove(listener); + } + + /** + * Callback for receiving assist data + */ + public interface AssistDataListener { + + void onAssistDataReceived(int taskId); + } +} diff --git a/quickstep/src/com/android/quickstep/RemoteRunnable.java b/quickstep/src/com/android/quickstep/RemoteRunnable.java new file mode 100644 index 0000000000..ec7cad4bad --- /dev/null +++ b/quickstep/src/com/android/quickstep/RemoteRunnable.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.os.RemoteException; +import android.util.Log; + +@FunctionalInterface +public interface RemoteRunnable { + + void run() throws RemoteException; + + static void executeSafely(RemoteRunnable r) { + try { + r.run(); + } catch (final RemoteException e) { + Log.e("RemoteRunnable", "Error calling remote method", e); + } + } +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java new file mode 100644 index 0000000000..66969c65a5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import android.content.Context; +import android.graphics.Matrix; +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +/** + * Factory class to create and add an overlays on the TaskView + */ +public class TaskOverlayFactory { + + private static TaskOverlayFactory sInstance; + + public static TaskOverlayFactory get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(TaskOverlayFactory.class, + context.getApplicationContext(), R.string.task_overlay_factory_class); + } + return sInstance; + } + + public TaskOverlay createOverlay(View thumbnailView) { + return new TaskOverlay(); + } + + public static class TaskOverlay { + + public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) { } + + public void reset() { } + + } +} diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java new file mode 100644 index 0000000000..f82ff8c535 --- /dev/null +++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.InstantAppResolver; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.ISystemUiProxy; +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.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.Consumer; + +/** + * Represents a system shortcut that can be shown for a recent task. + */ +public class TaskSystemShortcut extends SystemShortcut { + + private static final String TAG = "TaskSystemShortcut"; + + protected T mSystemShortcut; + + protected TaskSystemShortcut(T systemShortcut) { + super(systemShortcut.iconResId, systemShortcut.labelResId); + mSystemShortcut = systemShortcut; + } + + protected TaskSystemShortcut(int iconResId, int labelResId) { + super(iconResId, labelResId); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, ItemInfo itemInfo) { + return null; + } + + public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) { + Task task = view.getTask(); + + ShortcutInfo dummyInfo = new ShortcutInfo(); + dummyInfo.intent = new Intent(); + ComponentName component = task.getTopComponent(); + dummyInfo.intent.setComponent(component); + dummyInfo.user = UserHandle.of(task.key.userId); + dummyInfo.title = TaskUtils.getTitle(activity, task); + + return getOnClickListenerForTask(activity, task, dummyInfo); + } + + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) { + return mSystemShortcut.getOnClickListener(activity, dummyInfo); + } + + public static class AppInfo extends TaskSystemShortcut { + public AppInfo() { + super(new SystemShortcut.AppInfo()); + } + } + + public static class SplitScreen extends TaskSystemShortcut { + + private Handler mHandler; + + public SplitScreen() { + super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + if (activity.getDeviceProfile().isMultiWindowMode) { + return null; + } + final Task task = taskView.getTask(); + final int taskId = task.key.id; + if (!task.isDockable) { + return null; + } + final RecentsView recentsView = activity.getOverviewPanel(); + + final TaskThumbnailView thumbnailView = taskView.getThumbnail(); + return (v -> { + 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) { + taskView.getRootView().removeOnLayoutChangeListener(this); + recentsView.removeIgnoreResetTask(taskView); + + // Start animating in the side pages once launcher has been resized + recentsView.dismissTask(taskView, false, false); + } + }; + + final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = + new DeviceProfile.OnDeviceProfileChangeListener() { + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + activity.removeOnDeviceProfileChangeListener(this); + if (dp.isMultiWindowMode) { + taskView.getRootView().addOnLayoutChangeListener( + onLayoutChangeListener); + } + } + }; + + AbstractFloatingView.closeOpenViews(activity, true, + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + + final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(); + if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { + return; + } + boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; + if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, + ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft))) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + try { + sysUiProxy.onSplitScreenInvoked(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify SysUI of split screen: ", e); + return; + } + activity.getUserEventDispatcher().logActionOnControl(TAP, + LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET); + // Add a device profile change listener to kick off animating the side tasks + // once we enter multiwindow mode and relayout + activity.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 + recentsView.addIgnoreResetTask(taskView); + taskView.setAlpha(0f); + }; + + final int[] position = new int[2]; + thumbnailView.getLocationOnScreen(position); + final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX()); + final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY()); + final Rect taskBounds = new Rect(position[0], position[1], + position[0] + width, position[1] + height); + + Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( + taskBounds.width(), taskBounds.height(), thumbnailView, 1f, + Color.BLACK); + AppTransitionAnimationSpecsFuture future = + new AppTransitionAnimationSpecsFuture(mHandler) { + @Override + public List composeSpecs() { + return Collections.singletonList(new AppTransitionAnimationSpecCompat( + taskId, thumbnail, taskBounds)); + } + }; + WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( + future, animStartedListener, mHandler, true /* scaleUp */); + } + }); + } + } + + public static class Pin extends TaskSystemShortcut { + + private static final String TAG = Pin.class.getSimpleName(); + + private Handler mHandler; + + public Pin() { + super(R.drawable.ic_pin, R.string.recent_task_option_pin); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy == null) { + 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 view -> { + Consumer resultCallback = success -> { + if (success) { + try { + sysUiProxy.startScreenPinning(taskView.getTask().key.id); + } catch (RemoteException e) { + Log.w(TAG, "Failed to start screen pinning: ", e); + } + } else { + taskView.notifyTaskLaunchFailed(TAG); + } + }; + taskView.launchTask(true, resultCallback, mHandler); + }; + } + } + + public static class Install extends TaskSystemShortcut { + public Install() { + super(new SystemShortcut.Install()); + } + + @Override + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo itemInfo) { + if (InstantAppResolver.newInstance(activity).isInstantApp(activity, + task.getTopComponent().getPackageName())) { + return mSystemShortcut.createOnClickListener(activity, itemInfo); + } + return null; + } + } +} diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java new file mode 100644 index 0000000000..2b0c98f939 --- /dev/null +++ b/quickstep/src/com/android/quickstep/TaskUtils.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber; +import static com.android.systemui.shared.recents.utilities.Utilities.getSurface; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +import android.animation.ValueAnimator; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.RectF; +import android.os.UserHandle; +import android.util.Log; +import android.view.Surface; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationTargetSet; +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.system.RemoteAnimationTargetCompat; + +import java.util.List; + +/** + * Contains helpful methods for retrieving data from {@link Task}s. + */ +public class TaskUtils { + + private static final String TAG = "TaskUtils"; + + /** + * TODO: remove this once we switch to getting the icon and label from IconCache. + */ + public static CharSequence getTitle(Context context, Task task) { + LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context); + UserManagerCompat userManagerCompat = UserManagerCompat.getInstance(context); + PackageManager packageManager = context.getPackageManager(); + UserHandle user = UserHandle.of(task.key.userId); + ApplicationInfo applicationInfo = launcherAppsCompat.getApplicationInfo( + task.getTopComponent().getPackageName(), 0, user); + if (applicationInfo == null) { + Log.e(TAG, "Failed to get title for task " + task); + return ""; + } + return userManagerCompat.getBadgedLabelForUser( + applicationInfo.loadLabel(packageManager), user); + } + + public static ComponentKey getComponentKeyForTask(Task.TaskKey taskKey) { + return new ComponentKey(taskKey.getComponent(), UserHandle.of(taskKey.userId)); + } + + + /** + * Try to find a TaskView that corresponds with the component of the launched view. + * + * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation. + * Otherwise, we will assume we are using a normal app transition, but it's possible that the + * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView. + */ + public static TaskView findTaskViewToLaunch( + BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) { + if (v instanceof TaskView) { + return (TaskView) v; + } + RecentsView recentsView = activity.getOverviewPanel(); + + // It's possible that the launched view can still be resolved to a visible task view, check + // the task id of the opening task and see if we can find a match. + if (v.getTag() instanceof ItemInfo) { + ItemInfo itemInfo = (ItemInfo) v.getTag(); + ComponentName componentName = itemInfo.getTargetComponent(); + int userId = itemInfo.user.getIdentifier(); + if (componentName != null) { + for (int i = 0; i < recentsView.getChildCount(); i++) { + TaskView taskView = recentsView.getPageAt(i); + if (recentsView.isTaskViewVisible(taskView)) { + Task.TaskKey key = taskView.getTask().key; + if (componentName.equals(key.getComponent()) && userId == key.userId) { + return taskView; + } + } + } + } + } + + if (targets == null) { + return null; + } + // Resolve the opening task id + int openingTaskId = -1; + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + openingTaskId = target.taskId; + break; + } + } + + // If there is no opening task id, fall back to the normal app icon launch animation + if (openingTaskId == -1) { + return null; + } + + // If the opening task id is not currently visible in overview, then fall back to normal app + // icon launch animation + TaskView taskView = recentsView.getTaskView(openingTaskId); + if (taskView == null || !recentsView.isTaskViewVisible(taskView)) { + return null; + } + return taskView; + } + + /** + * @return Animator that controls the window of the opening targets for the recents launch + * animation. + */ + public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges, + RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) { + final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + + // Defer fading out the view until after the app window gets faded in + final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR); + final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR); + + final RemoteAnimationTargetSet mTargetSet; + + final RectF mThumbnailRect; + private Surface mSurface; + private long mFrameNumber; + + { + mTargetSet = new RemoteAnimationTargetSet(targets, MODE_OPENING); + inOutHelper.setTaskTransformCallback((t, app) -> { + t.setAlpha(app.leash, mTaskAlpha.value); + + if (!skipViewChanges) { + t.deferTransactionUntil(app.leash, mSurface, mFrameNumber); + } + }); + + inOutHelper.prepareAnimation(true /* isOpening */); + inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), + mTargetSet.apps.length == 0 ? null : mTargetSet.apps[0]); + + mThumbnailRect = new RectF(inOutHelper.getTargetRect()); + mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY()); + Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX()); + } + + @Override + public void onUpdate(float percent) { + mSurface = getSurface(v); + mFrameNumber = mSurface != null ? getNextFrameNumber(mSurface) : -1; + if (mFrameNumber == -1) { + // Booo, not cool! Our surface got destroyed, so no reason to animate anything. + Log.w(TAG, "Failed to animate, surface got destroyed."); + return; + } + + RectF taskBounds = inOutHelper.applyTransform(mTargetSet, 1 - percent); + if (!skipViewChanges) { + float scale = taskBounds.width() / mThumbnailRect.width(); + v.setScaleX(scale); + v.setScaleY(scale); + v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX()); + v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY()); + v.setAlpha(mViewAlpha.value); + } + } + }); + return appAnimator; + } + + public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets, + int taskId, int mode) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == mode && target.taskId == taskId) { + return true; + } + } + return false; + } + + public static boolean checkCurrentOrManagedUserId(int currentUserId, Context context) { + if (currentUserId == UserHandle.myUserId()) { + return true; + } + List allUsers = UserManagerCompat.getInstance(context).getUserProfiles(); + for (int i = allUsers.size() - 1; i >= 0; i--) { + if (currentUserId == allUsers.get(i).getIdentifier()) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/TouchConsumer.java b/quickstep/src/com/android/quickstep/TouchConsumer.java new file mode 100644 index 0000000000..4cecffa2be --- /dev/null +++ b/quickstep/src/com/android/quickstep/TouchConsumer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.IntDef; +import android.view.Choreographer; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; + +@TargetApi(Build.VERSION_CODES.O) +@FunctionalInterface +public interface TouchConsumer extends Consumer { + + @IntDef(flag = true, value = { + INTERACTION_NORMAL, + INTERACTION_QUICK_SCRUB + }) + @Retention(RetentionPolicy.SOURCE) + @interface InteractionType {} + int INTERACTION_NORMAL = 0; + int INTERACTION_QUICK_SCRUB = 1; + + default void reset() { } + + default void updateTouchTracking(@InteractionType int interactionType) { } + + default void onQuickScrubEnd() { } + + default void onQuickScrubProgress(float progress) { } + + default void onQuickStep(MotionEvent ev) { } + + default void onCommand(int command) { } + + /** + * Called on the binder thread to allow the consumer to process the motion event before it is + * posted on a handler thread. + */ + default void preProcessMotionEvent(MotionEvent ev) { } + + default Choreographer getIntrimChoreographer(MotionEventQueue queue) { + return null; + } + + default void deferInit() { } + + default boolean deferNextEventToMainThread() { + return false; + } + + default boolean forceToLauncherConsumer() { + return false; + } + + default void onShowOverviewFromAltTab() {} +} diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java new file mode 100644 index 0000000000..aecb66c77d --- /dev/null +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.Service; +import android.content.Intent; +import android.graphics.PointF; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.views.RecentsView; +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.NavigationBarCompat.HitTarget; + +/** + * Service connected by system-UI for handling touch interaction. + */ +@TargetApi(Build.VERSION_CODES.O) +public class TouchInteractionService extends Service { + + private static final SparseArray sMotionEventNames; + + static { + sMotionEventNames = new SparseArray<>(3); + sMotionEventNames.put(ACTION_DOWN, "ACTION_DOWN"); + sMotionEventNames.put(ACTION_UP, "ACTION_UP"); + sMotionEventNames.put(ACTION_CANCEL, "ACTION_CANCEL"); + } + + public static final int EDGE_NAV_BAR = 1 << 8; + + private static final String TAG = "TouchInteractionService"; + + /** + * A background thread used for handling UI for another window. + */ + private static HandlerThread sRemoteUiThread; + + private final IBinder mMyBinder = new IOverviewProxy.Stub() { + + @Override + public void onPreMotionEvent(@HitTarget int downHitTarget) throws RemoteException { + TraceHelper.beginSection("SysUiBinder"); + setupTouchConsumer(downHitTarget); + TraceHelper.partitionSection("SysUiBinder", "Down target " + downHitTarget); + } + + @Override + public void onMotionEvent(MotionEvent ev) { + mEventQueue.queue(ev); + + String name = sMotionEventNames.get(ev.getActionMasked()); + if (name != null){ + TraceHelper.partitionSection("SysUiBinder", name); + } + } + + @Override + public void onBind(ISystemUiProxy iSystemUiProxy) { + mISystemUiProxy = iSystemUiProxy; + mRecentsModel.setSystemUiProxy(mISystemUiProxy); + mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy); + } + + @Override + public void onQuickScrubStart() { + mEventQueue.onQuickScrubStart(); + TraceHelper.partitionSection("SysUiBinder", "onQuickScrubStart"); + } + + @Override + public void onQuickScrubProgress(float progress) { + mEventQueue.onQuickScrubProgress(progress); + } + + @Override + public void onQuickScrubEnd() { + mEventQueue.onQuickScrubEnd(); + TraceHelper.endSection("SysUiBinder", "onQuickScrubEnd"); + } + + @Override + public void onOverviewToggle() { + mOverviewCommandHelper.onOverviewToggle(); + } + + @Override + public void onOverviewShown(boolean triggeredFromAltTab) { + if (triggeredFromAltTab) { + setupTouchConsumer(HIT_TARGET_NONE); + mEventQueue.onOverviewShownFromAltTab(); + } else { + mOverviewCommandHelper.onOverviewShown(); + } + } + + @Override + public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + if (triggeredFromAltTab && !triggeredFromHomeKey) { + // onOverviewShownFromAltTab initiates quick scrub. Ending it here. + mEventQueue.onQuickScrubEnd(); + } + } + + @Override + public void onQuickStep(MotionEvent motionEvent) { + mEventQueue.onQuickStep(motionEvent); + TraceHelper.endSection("SysUiBinder", "onQuickStep"); + + } + + @Override + public void onTip(int actionType, int viewType) { + mOverviewCommandHelper.onTip(actionType, viewType); + } + }; + + private final TouchConsumer mNoOpTouchConsumer = (ev) -> {}; + + private static boolean sConnected = false; + + public static boolean isConnected() { + return sConnected; + } + + private ActivityManagerWrapper mAM; + private RecentsModel mRecentsModel; + private MotionEventQueue mEventQueue; + private MainThreadExecutor mMainThreadExecutor; + private ISystemUiProxy mISystemUiProxy; + private OverviewCommandHelper mOverviewCommandHelper; + private OverviewInteractionState mOverviewInteractionState; + private OverviewCallbacks mOverviewCallbacks; + + private Choreographer mMainThreadChoreographer; + private Choreographer mBackgroundThreadChoreographer; + + @Override + public void onCreate() { + super.onCreate(); + mAM = ActivityManagerWrapper.getInstance(); + mRecentsModel = RecentsModel.getInstance(this); + mRecentsModel.setPreloadTasksInBackground(true); + mMainThreadExecutor = new MainThreadExecutor(); + mOverviewCommandHelper = new OverviewCommandHelper(this); + mMainThreadChoreographer = Choreographer.getInstance(); + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer); + mOverviewInteractionState = OverviewInteractionState.getInstance(this); + mOverviewCallbacks = OverviewCallbacks.get(this); + + sConnected = true; + + // Temporarily disable model preload + // new ModelPreload().start(this); + initBackgroundChoreographer(); + } + + @Override + public void onDestroy() { + mOverviewCommandHelper.onDestroy(); + sConnected = false; + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "Touch service connected"); + return mMyBinder; + } + + private void setupTouchConsumer(@HitTarget int downHitTarget) { + mEventQueue.reset(); + TouchConsumer oldConsumer = mEventQueue.getConsumer(); + if (oldConsumer.deferNextEventToMainThread()) { + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, + new DeferredTouchConsumer((v) -> getCurrentTouchConsumer(downHitTarget, + oldConsumer.forceToLauncherConsumer(), v))); + mEventQueue.deferInit(); + } else { + mEventQueue = new MotionEventQueue( + mMainThreadChoreographer, getCurrentTouchConsumer(downHitTarget, false, null)); + } + } + + private TouchConsumer getCurrentTouchConsumer( + @HitTarget int downHitTarget, boolean forceToLauncher, VelocityTracker tracker) { + RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0); + + if (runningTaskInfo == null && !forceToLauncher) { + return mNoOpTouchConsumer; + } else if (forceToLauncher || + runningTaskInfo.topActivity.equals(mOverviewCommandHelper.overviewComponent)) { + return getOverviewConsumer(); + } else { + if (tracker == null) { + tracker = VelocityTracker.obtain(); + } + return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel, + mOverviewCommandHelper.overviewIntent, + mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor, + mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks, + tracker); + } + } + + private TouchConsumer getOverviewConsumer() { + ActivityControlHelper activityHelper = mOverviewCommandHelper.getActivityControlHelper(); + BaseDraggingActivity activity = activityHelper.getCreatedActivity(); + if (activity == null) { + return mNoOpTouchConsumer; + } + return new OverviewTouchConsumer(activityHelper, activity); + } + + private static class OverviewTouchConsumer + implements TouchConsumer { + + private final ActivityControlHelper mActivityHelper; + private final T mActivity; + private final BaseDragLayer mTarget; + private final int[] mLocationOnScreen = new int[2]; + private final PointF mDownPos = new PointF(); + private final int mTouchSlop; + private final QuickScrubController mQuickScrubController; + + private boolean mTrackingStarted = false; + private boolean mInvalidated = false; + + private float mLastProgress = 0; + private boolean mStartPending = false; + private boolean mEndPending = false; + + OverviewTouchConsumer(ActivityControlHelper activityHelper, T activity) { + mActivityHelper = activityHelper; + mActivity = activity; + mTarget = activity.getDragLayer(); + mTouchSlop = ViewConfiguration.get(mTarget.getContext()).getScaledTouchSlop(); + + mQuickScrubController = mActivity.getOverviewPanel() + .getQuickScrubController(); + } + + @Override + public void accept(MotionEvent ev) { + if (mInvalidated) { + return; + } + int action = ev.getActionMasked(); + if (action == ACTION_DOWN) { + mTrackingStarted = false; + mDownPos.set(ev.getX(), ev.getY()); + } else if (!mTrackingStarted) { + switch (action) { + case ACTION_POINTER_UP: + case ACTION_POINTER_DOWN: + if (!mTrackingStarted) { + mInvalidated = true; + } + break; + case ACTION_MOVE: { + float displacement = ev.getY() - mDownPos.y; + if (Math.abs(displacement) >= mTouchSlop) { + mTarget.getLocationOnScreen(mLocationOnScreen); + + // Send a down event only when mTouchSlop is crossed. + MotionEvent down = MotionEvent.obtain(ev); + down.setAction(ACTION_DOWN); + sendEvent(down); + down.recycle(); + mTrackingStarted = true; + } + } + } + } + + if (mTrackingStarted) { + sendEvent(ev); + } + + if (action == ACTION_UP || action == ACTION_CANCEL) { + mInvalidated = true; + } + } + + private void sendEvent(MotionEvent ev) { + int flags = ev.getEdgeFlags(); + ev.setEdgeFlags(flags | EDGE_NAV_BAR); + ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); + if (!mTrackingStarted) { + mTarget.onInterceptTouchEvent(ev); + } + mTarget.onTouchEvent(ev); + ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]); + ev.setEdgeFlags(flags); + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mInvalidated) { + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + } + + @Override + public void updateTouchTracking(int interactionType) { + if (mInvalidated) { + return; + } + if (interactionType == INTERACTION_QUICK_SCRUB) { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + mStartPending = true; + Runnable action = () -> { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + mActivityHelper.onQuickInteractionStart(mActivity, null, true); + mQuickScrubController.onQuickScrubProgress(mLastProgress); + mStartPending = false; + + if (mEndPending) { + mQuickScrubController.onQuickScrubEnd(); + mEndPending = false; + } + }; + + mActivityHelper.executeOnWindowAvailable(mActivity, action); + } + } + + @Override + public void onQuickScrubEnd() { + if (mInvalidated) { + return; + } + if (mStartPending) { + mEndPending = true; + } else { + mQuickScrubController.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + mLastProgress = progress; + if (mInvalidated || mStartPending) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + } + + private void initBackgroundChoreographer() { + if (sRemoteUiThread == null) { + sRemoteUiThread = new HandlerThread("remote-ui"); + sRemoteUiThread.start(); + } + new Handler(sRemoteUiThread.getLooper()).post(() -> + mBackgroundThreadChoreographer = Choreographer.getInstance()); + } +} diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java new file mode 100644 index 0000000000..b1663b169a --- /dev/null +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -0,0 +1,961 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.UserHandle; +import android.support.annotation.AnyThread; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver.OnDrawListener; +import android.view.WindowManager; +import android.view.animation.Interpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +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.ActivityManagerWrapper; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.WindowCallbacksCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.StringJoiner; + +@TargetApi(Build.VERSION_CODES.O) +public class WindowTransformSwipeHandler { + private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); + private static final boolean DEBUG_STATES = false; + + // Launcher UI related states + private static final int STATE_LAUNCHER_PRESENT = 1 << 0; + private static final int STATE_LAUNCHER_STARTED = 1 << 1; + private static final int STATE_LAUNCHER_DRAWN = 1 << 2; + private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 3; + + // Internal initialization states + private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4; + + // Interaction finish states + private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5; + private static final int STATE_SCALED_CONTROLLER_APP = 1 << 6; + + private static final int STATE_HANDLER_INVALIDATED = 1 << 7; + private static final int STATE_GESTURE_STARTED = 1 << 8; + private static final int STATE_GESTURE_CANCELLED = 1 << 9; + private static final int STATE_GESTURE_COMPLETED = 1 << 10; + + // States for quick switch/scrub + private static final int STATE_CURRENT_TASK_FINISHED = 1 << 11; + private static final int STATE_QUICK_SCRUB_START = 1 << 12; + private static final int STATE_QUICK_SCRUB_END = 1 << 13; + + private static final int STATE_CAPTURE_SCREENSHOT = 1 << 14; + private static final int STATE_SCREENSHOT_CAPTURED = 1 << 15; + + private static final int STATE_RESUME_LAST_TASK = 1 << 16; + + private static final int LAUNCHER_UI_STATES = + STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_LAUNCHER_STARTED; + + private static final int LONG_SWIPE_ENTER_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED; + + private static final int LONG_SWIPE_START_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; + + // For debugging, keep in sync with above states + private static final String[] STATES = new String[] { + "STATE_LAUNCHER_PRESENT", + "STATE_LAUNCHER_STARTED", + "STATE_LAUNCHER_DRAWN", + "STATE_ACTIVITY_MULTIPLIER_COMPLETE", + "STATE_APP_CONTROLLER_RECEIVED", + "STATE_SCALED_CONTROLLER_RECENTS", + "STATE_SCALED_CONTROLLER_APP", + "STATE_HANDLER_INVALIDATED", + "STATE_GESTURE_STARTED", + "STATE_GESTURE_CANCELLED", + "STATE_GESTURE_COMPLETED", + "STATE_CURRENT_TASK_FINISHED", + "STATE_QUICK_SCRUB_START", + "STATE_QUICK_SCRUB_END", + "STATE_CAPTURE_SCREENSHOT", + "STATE_SCREENSHOT_CAPTURED", + "STATE_RESUME_LAST_TASK", + }; + + public static final long MAX_SWIPE_DURATION = 350; + public static final long MIN_SWIPE_DURATION = 80; + + private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f; + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); + + private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper(); + + protected Runnable mGestureEndCallback; + protected boolean mIsGoingToHome; + private DeviceProfile mDp; + private int mTransitionDragLength; + + // Shift in the range of [0, 1]. + // 0 => preview snapShot is completely visible, and hotseat is completely translated down + // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely + // visible. + private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + + // An increasing identifier per single instance of OtherActivityTouchConsumer. Generally one + // instance of OtherActivityTouchConsumer will only have one swipe handle, but sometimes we can + // end up with multiple handlers if we get recents command in the middle of a swipe gesture. + // This is used to match the corresponding activity manager callbacks in + // OtherActivityTouchConsumer + public final int id; + private final Context mContext; + private final ActivityControlHelper mActivityControlHelper; + private final ActivityInitListener mActivityInitListener; + + private final int mRunningTaskId; + private final RunningTaskInfo mRunningTaskInfo; + private ThumbnailData mTaskSnapshot; + + private MultiStateCallback mStateCallback; + private AnimatorPlaybackController mLauncherTransitionController; + + private T mActivity; + private LayoutListener mLayoutListener; + private RecentsView mRecentsView; + private QuickScrubController mQuickScrubController; + private AnimationFactory mAnimationFactory = (t, i) -> { }; + + private Runnable mLauncherDrawnCallback; + + private boolean mWasLauncherAlreadyVisible; + + private boolean mGestureStarted; + private int mLogAction = Touch.SWIPE; + private float mCurrentQuickScrubProgress; + private boolean mQuickScrubBlocked; + + private @InteractionType int mInteractionType = INTERACTION_NORMAL; + + private InputConsumerController mInputConsumer = + InputConsumerController.getRecentsAnimationInputConsumer(); + + private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper(); + + private final long mTouchTimeMs; + private long mLauncherFrameDrawnTime; + + private boolean mBgLongSwipeMode = false; + private boolean mUiLongSwipeMode = false; + private float mLongSwipeDisplacement = 0; + private LongSwipeHelper mLongSwipeController; + + WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, + long touchTimeMs, ActivityControlHelper controller) { + this.id = id; + mContext = context; + mRunningTaskInfo = runningTaskInfo; + mRunningTaskId = runningTaskInfo.id; + mTouchTimeMs = touchTimeMs; + mActivityControlHelper = controller; + mActivityInitListener = mActivityControlHelper + .createActivityInitListener(this::onActivityInit); + + initStateCallbacks(); + // Register the input consumer on the UI thread, to ensure that it runs after any pending + // unregister calls + executeOnUiThread(mInputConsumer::registerInputConsumer); + } + + private void initStateCallbacks() { + mStateCallback = new MultiStateCallback() { + @Override + public void setState(int stateFlag) { + debugNewState(stateFlag); + super.setState(stateFlag); + } + }; + + mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED, + this::initializeLauncherAnimationController); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, + this::launcherFrameDrawn); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, + this::notifyGestureStartedAsync); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED + | STATE_GESTURE_CANCELLED, + this::resetStateForAnimationCancel); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, + this::sendRemoteAnimationsToAnimationFactory); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP, + this::resumeLastTaskForQuickstep); + mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, + this::resumeLastTask); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_CAPTURE_SCREENSHOT, + this::switchToScreenshot); + + mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED + | STATE_SCALED_CONTROLLER_RECENTS, + this::finishCurrentTransitionToHome); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_SCALED_CONTROLLER_RECENTS + | STATE_CURRENT_TASK_FINISHED + | STATE_GESTURE_COMPLETED, + this::setupLauncherUiAfterSwipeUpAnimation); + + mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, + this::invalidateHandlerWithLauncher); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED + | STATE_SCALED_CONTROLLER_APP, + this::notifyTransitionCancelled); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_APP_CONTROLLER_RECEIVED, this::onQuickScrubStart); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED + | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); + + mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); + mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); + } + + private void executeOnUiThread(Runnable action) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + action.run(); + } else { + postAsyncCallback(mMainThreadHandler, action); + } + } + + private void setStateOnUiThread(int stateFlag) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + mStateCallback.setState(stateFlag); + } else { + postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); + } + } + + private void initTransitionEndpoints(DeviceProfile dp) { + mDp = dp; + + TransformedRect tempRect = new TransformedRect(); + mTransitionDragLength = mActivityControlHelper + .getSwipeUpDestinationAndLength(dp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + } + + private long getFadeInDuration() { + if (mCurrentShift.getCurrentAnimation() != null) { + ObjectAnimator anim = mCurrentShift.getCurrentAnimation(); + long theirDuration = anim.getDuration() - anim.getCurrentPlayTime(); + + // TODO: Find a better heuristic + return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION)); + } else { + return MAX_SWIPE_DURATION; + } + } + + public void initWhenReady() { + mActivityInitListener.register(); + } + + private boolean onActivityInit(final T activity, Boolean alreadyOnHome) { + if (mActivity == activity) { + return true; + } + if (mActivity != null) { + // The launcher may have been recreated as a result of device rotation. + int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; + initStateCallbacks(); + mStateCallback.setState(oldState); + mLayoutListener.setHandler(null); + } + mWasLauncherAlreadyVisible = alreadyOnHome; + mActivity = activity; + // 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(INVISIBLE_BY_STATE_HANDLER); + } else { + mActivity.addForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } + + mRecentsView = activity.getOverviewPanel(); + mQuickScrubController = mRecentsView.getQuickScrubController(); + mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity); + + mStateCallback.setState(STATE_LAUNCHER_PRESENT); + if (alreadyOnHome) { + onLauncherStart(activity); + } else { + activity.setOnStartCallback(this::onLauncherStart); + } + return true; + } + + private void onLauncherStart(final T activity) { + if (mActivity != activity) { + return; + } + if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { + return; + } + + mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, + mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated); + AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); + + if (mWasLauncherAlreadyVisible) { + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); + } else { + TraceHelper.beginSection("WTS-init"); + View dragLayer = activity.getDragLayer(); + mActivityControlHelper.getAlphaProperty(activity).setValue(0); + dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { + + @Override + public void onDraw() { + TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); + dragLayer.post(() -> + dragLayer.getViewTreeObserver().removeOnDrawListener(this)); + if (activity != mActivity) { + return; + } + + mStateCallback.setState(STATE_LAUNCHER_DRAWN); + } + }); + } + + mRecentsView.showTask(mRunningTaskId); + mRecentsView.setRunningTaskHidden(true); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + mLayoutListener.open(); + mStateCallback.setState(STATE_LAUNCHER_STARTED); + } + + public void setLauncherOnDrawCallback(Runnable callback) { + mLauncherDrawnCallback = callback; + } + + private void launcherFrameDrawn() { + AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); + if (property.getValue() < 1) { + if (mGestureStarted) { + final MultiStateCallback callback = mStateCallback; + ObjectAnimator animator = ObjectAnimator.ofFloat( + property, MultiValueAlpha.VALUE, 1); + animator.setDuration(getFadeInDuration()).addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + }); + animator.start(); + } else { + property.setValue(1); + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + } + if (mLauncherDrawnCallback != null) { + mLauncherDrawnCallback.run(); + } + mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); + } + + private void sendRemoteAnimationsToAnimationFactory() { + mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); + } + + private void initializeLauncherAnimationController() { + mLayoutListener.setHandler(this); + buildAnimationController(); + + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); + } + } + + public void updateInteractionType(@InteractionType int interactionType) { + if (mInteractionType != INTERACTION_NORMAL) { + throw new IllegalArgumentException( + "Can't change interaction type from " + mInteractionType); + } + if (interactionType != INTERACTION_QUICK_SCRUB) { + throw new IllegalArgumentException( + "Can't change interaction type to " + interactionType); + } + mInteractionType = interactionType; + mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub); + + setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); + + // Start the window animation without waiting for launcher. + animateToProgress(mCurrentShift.value, 1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR); + } + + private void shiftAnimationDestinationForQuickscrub() { + TransformedRect tempRect = new TransformedRect(); + mActivityControlHelper + .getSwipeUpDestinationAndLength(mDp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + + float offsetY = + mActivityControlHelper.getTranslationYForQuickScrub(tempRect, mDp, mContext); + float scale, offsetX; + Resources res = mContext.getResources(); + + if (ActivityManagerWrapper.getInstance().getRecentTasks(2, UserHandle.myUserId()).size() + < 2) { + // There are not enough tasks, we don't need to shift + offsetX = 0; + scale = 1; + } else { + offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing) + + tempRect.rect.width(); + float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 + + res.getDimensionPixelSize(R.dimen.recents_page_spacing); + float interpolation = Math.min(1, offsetX / distanceToReachEdge); + scale = TaskView.getCurveScaleForInterpolation(interpolation); + } + mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY, + QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); + } + + @WorkerThread + public void updateDisplacement(float displacement) { + // We are moving in the negative x/y direction + displacement = -displacement; + if (displacement > mTransitionDragLength) { + mCurrentShift.updateValue(1); + + if (!mBgLongSwipeMode) { + mBgLongSwipeMode = true; + executeOnUiThread(this::onLongSwipeEnabledUi); + } + mLongSwipeDisplacement = displacement - mTransitionDragLength; + executeOnUiThread(this::onLongSwipeDisplacementUpdated); + } else { + if (mBgLongSwipeMode) { + mBgLongSwipeMode = false; + executeOnUiThread(this::onLongSwipeDisabledUi); + } + float translation = Math.max(displacement, 0); + float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; + mCurrentShift.updateValue(shift); + } + } + + /** + * Called by {@link #mLayoutListener} when launcher layout changes + */ + public void buildAnimationController() { + initTransitionEndpoints(mActivity.getDeviceProfile()); + mAnimationFactory.createActivityController(mTransitionDragLength, mInteractionType); + } + + private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { + mLauncherTransitionController = anim; + mLauncherTransitionController.dispatchOnStart(); + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + @WorkerThread + private void updateFinalShift() { + float shift = mCurrentShift.value; + + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, shift); + + // TODO: This logic is spartanic! + boolean passedThreshold = shift > 0.12f; + mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); + if (mActivityControlHelper.shouldMinimizeSplitScreen()) { + mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); + } + } + + executeOnUiThread(this::updateFinalShiftUi); + } + + private void updateFinalShiftUi() { + if (mLauncherTransitionController == null) { + return; + } + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, + RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + InvariantDeviceProfile idp = appState == null ? + new InvariantDeviceProfile(mContext) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(mContext); + final Rect overviewStackBounds; + RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); + + if (minimizedHomeBounds != null && runningTaskTarget != null) { + overviewStackBounds = mActivityControlHelper + .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); + dp = dp.getMultiWindowProfile(mContext, + new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); + dp.updateInsets(homeContentInsets); + } else { + overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); + // If we are not in multi-window mode, home insets should be same as system insets. + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + dp = dp.copy(mContext); + dp.updateInsets(insets); + } + dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); + + if (runningTaskTarget != null) { + mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); + } + mClipAnimationHelper.prepareAnimation(false /* isOpening */); + initTransitionEndpoints(dp); + + mRecentsAnimationWrapper.setController(controller, targets); + setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); + } + + public void onRecentsAnimationCanceled() { + mRecentsAnimationWrapper.setController(null, null); + mActivityInitListener.unregister(); + setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); + } + + public void onGestureStarted() { + notifyGestureStartedAsync(); + setStateOnUiThread(STATE_GESTURE_STARTED); + mGestureStarted = true; + mRecentsAnimationWrapper.hideCurrentInputMethod(); + mRecentsAnimationWrapper.enableInputConsumer(); + } + + /** + * Notifies the launcher that the swipe gesture has started. This can be called multiple times + * on both background and UI threads + */ + @AnyThread + private void notifyGestureStartedAsync() { + final T curActivity = mActivity; + if (curActivity != null) { + // Once the gesture starts, we can no longer transition home through the button, so + // reset the force override of the activity visibility + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } + } + + @WorkerThread + public void onGestureEnded(float endVelocity) { + float flingThreshold = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_threshold_velocity); + boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; + setStateOnUiThread(STATE_GESTURE_COMPLETED); + + mLogAction = isFling ? Touch.FLING : Touch.SWIPE; + + if (mBgLongSwipeMode) { + executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling)); + } else { + handleNormalGestureEnd(endVelocity, isFling); + } + } + + private void handleNormalGestureEnd(float endVelocity, boolean isFling) { + long duration = MAX_SWIPE_DURATION; + final float endShift; + final float startShift; + if (!isFling) { + endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted ? 1 : 0; + long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + startShift = mCurrentShift.value; + } else { + endShift = endVelocity < 0 ? 1 : 0; + float minFlingVelocity = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { + float distanceToTravel = (endShift - mCurrentShift.value) * 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(1000 * Math.abs(distanceToTravel / endVelocity)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + startShift = Utilities.boundToRange(mCurrentShift.value - endVelocity * SINGLE_FRAME_MS + / (mTransitionDragLength * 1000), 0, 1); + } + + animateToProgress(startShift, endShift, duration, DEACCEL); + } + + private void doLogGesture(boolean toLauncher) { + final int direction; + if (mDp.isVerticalBarLayout()) { + direction = (mDp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT; + } else { + direction = toLauncher ? Direction.UP : Direction.DOWN; + } + + int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP; + UserEventDispatcher.newInstance(mContext, mDp).logStateChangeAction( + mLogAction, direction, + ContainerType.NAVBAR, ContainerType.APP, + dstContainerType, + 0); + } + + /** Animates to the given progress, where 0 is the current app and 1 is overview. */ + private void animateToProgress(float start, float end, long duration, + Interpolator interpolator) { + mIsGoingToHome = Float.compare(end, 1) == 0; + ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration); + anim.setInterpolator(interpolator); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + setStateOnUiThread(mIsGoingToHome + ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT) + : STATE_SCALED_CONTROLLER_APP); + } + }); + mRecentsAnimationWrapper.runOnInit(anim::start); + } + + @UiThread + private void resumeLastTaskForQuickstep() { + setStateOnUiThread(STATE_RESUME_LAST_TASK); + doLogGesture(false /* toLauncher */); + reset(); + } + + @UiThread + private void resumeLastTask() { + mRecentsAnimationWrapper.finish(false /* toHome */, null); + } + + public void reset() { + if (mInteractionType != INTERACTION_QUICK_SCRUB) { + // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be + // invalidated after the quick scrub ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + } + + private void invalidateHandler() { + mCurrentShift.finishAnimation(); + + if (mGestureEndCallback != null) { + mGestureEndCallback.run(); + } + + mActivityInitListener.unregister(); + mInputConsumer.unregisterInputConsumer(); + mTaskSnapshot = null; + } + + private void invalidateHandlerWithLauncher() { + mLauncherTransitionController = null; + mLayoutListener.finish(); + mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); + + mRecentsView.setRunningTaskHidden(false); + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, false /* animate */); + mQuickScrubController.cancelActiveQuickscrub(); + } + + private void notifyTransitionCancelled() { + mAnimationFactory.onTransitionCancelled(); + } + + private void resetStateForAnimationCancel() { + boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; + mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); + } + + public void layoutListenerClosed() { + if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) { + mLauncherTransitionController.setPlayFraction(1); + } + } + + private void switchToScreenshot() { + boolean finishTransitionPosted = false; + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + // Update the screenshot of the task + if (mTaskSnapshot == null) { + mTaskSnapshot = controller.screenshotTask(mRunningTaskId); + } + TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); + mRecentsView.setRunningTaskHidden(false); + if (taskView != null) { + // Defer finishing the animation until the next launcher frame with the + // new thumbnail + finishTransitionPosted = new WindowCallbacksCompat(taskView) { + + @Override + public void onPostDraw(Canvas canvas) { + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + detach(); + } + }.attach(); + } + } + if (!finishTransitionPosted) { + // If we haven't posted a draw callback, set the state immediately. + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + } + } + + private void finishCurrentTransitionToHome() { + synchronized (mRecentsAnimationWrapper) { + mRecentsAnimationWrapper.finish(true /* toHome */, + () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); + } + } + + private void setupLauncherUiAfterSwipeUpAnimation() { + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + mActivityControlHelper.onSwipeUpComplete(mActivity); + + // Animate the first icon. + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); + mRecentsView.setSwipeDownShouldLaunchApp(true); + + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + + doLogGesture(true /* toLauncher */); + reset(); + } + + private void onQuickScrubStart() { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mQuickScrubBlocked = true; + setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); + return; + } + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + + mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false); + + // Inform the last progress in case we skipped before. + mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); + } + + private void onFinishedTransitionToQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onFinishedTransitionToQuickScrub(); + } + + public void onQuickScrubProgress(float progress) { + mCurrentQuickScrubProgress = progress; + if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null + || mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + public void onQuickScrubEnd() { + setStateOnUiThread(STATE_QUICK_SCRUB_END); + } + + private void switchToFinalAppAfterQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubEnd(); + + // Normally this is handled in reset(), but since we are still scrubbing after the + // transition into recents, we need to defer the handler invalidation for quick scrub until + // after the gesture ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + + private void debugNewState(int stateFlag) { + if (!DEBUG_STATES) { + return; + } + + int state = mStateCallback.getState(); + StringJoiner currentStateStr = new StringJoiner(", ", "[", "]"); + String stateFlagStr = "Unknown-" + stateFlag; + for (int i = 0; i < STATES.length; i++) { + if ((state & (i << i)) != 0) { + currentStateStr.add(STATES[i]); + } + if (stateFlag == (1 << i)) { + stateFlagStr = STATES[i] + " (" + stateFlag + ")"; + } + } + Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to " + + currentStateStr); + } + + public void setGestureEndCallback(Runnable gestureEndCallback) { + mGestureEndCallback = gestureEndCallback; + } + + // Handling long swipe + private void onLongSwipeEnabledUi() { + mUiLongSwipeMode = true; + checkLongSwipeCanEnter(); + checkLongSwipeCanStart(); + } + + private void onLongSwipeDisabledUi() { + mUiLongSwipeMode = false; + + if (mLongSwipeController != null) { + mLongSwipeController.destroy(); + + // Rebuild animations + buildAnimationController(); + } + } + + private void onLongSwipeDisplacementUpdated() { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + return; + } + + mLongSwipeController.onMove(mLongSwipeDisplacement); + } + + private void checkLongSwipeCanEnter() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + // We are entering long swipe mode, make sure the screen shot is captured. + mStateCallback.setState(STATE_CAPTURE_SCREENSHOT); + + } + + private void checkLongSwipeCanStart() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; + if (targetSet == null) { + // This can happen when cancelAnimation comes on the background thread, while we are + // processing the long swipe on the UI thread. + return; + } + + mLongSwipeController = mActivityControlHelper.getLongSwipeController( + mActivity, mRecentsAnimationWrapper.targetSet); + onLongSwipeDisplacementUpdated(); + } + + private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + mUiLongSwipeMode = false; + handleNormalGestureEnd(velocity, isFling); + return; + } + mUiLongSwipeMode = false; + finishCurrentTransitionToHome(); + mLongSwipeController.end(velocity, isFling, + () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); + + } +} diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java new file mode 100644 index 0000000000..9e2de33954 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.fallback; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.quickstep.RecentsActivity; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.views.RecentsView; + +public class FallbackRecentsView extends RecentsView { + + public FallbackRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOverviewStateEnabled(true); + getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + protected void onAllTasksRemoved() { + mActivity.startHome(); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + updateEmptyMessage(); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + // Just use the activity task size for multi-window as well. + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java new file mode 100644 index 0000000000..ca8c2520c4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.fallback; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.R; +import com.android.launcher3.util.Themes; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsActivity; + +public class RecentsRootView extends BaseDragLayer { + + private final RecentsActivity mActivity; + + private final Point mLastKnownSize = new Point(10, 10); + + public RecentsRootView(Context context, AttributeSet attrs) { + super(context, attrs, 1 /* alphaChannelCount */); + mActivity = (RecentsActivity) BaseActivity.fromContext(context); + setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + public Point getLastKnownSize() { + return mLastKnownSize; + } + + public void setup() { + mControllers = new TouchController[] { new RecentsTaskController(mActivity) }; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Check size changes before the actual measure, to avoid multiple measure calls. + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (mLastKnownSize.x != width || mLastKnownSize.y != height) { + mLastKnownSize.set(width, height); + mActivity.onRootViewSizeChanged(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @TargetApi(23) + @Override + protected boolean fitSystemWindows(Rect insets) { + // Update device profile before notifying the children. + mActivity.getDeviceProfile().updateInsets(insets); + setInsets(insets); + return true; // I'll take it from here + } + + @Override + public void setInsets(Rect insets) { + // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by + // modifying child layout params. + if (!insets.equals(mInsets)) { + super.setInsets(insets); + } + setBackground(insets.top == 0 ? null + : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim)); + } + + public void dispatchInsets() { + mActivity.getDeviceProfile().updateInsets(mInsets); + super.setInsets(mInsets); + } +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java new file mode 100644 index 0000000000..9463cc90f1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.fallback; + +import com.android.launcher3.uioverrides.TaskViewTouchController; +import com.android.quickstep.RecentsActivity; + +public class RecentsTaskController extends TaskViewTouchController { + + public RecentsTaskController(RecentsActivity activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.hasWindowFocus(); + } +} diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java new file mode 100644 index 0000000000..6dff187ea5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.logging; + +import android.content.Context; +import android.util.Log; + +import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CANCEL_TARGET; +import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE; +import static com.android.systemui.shared.system.LauncherEventUtil.DISMISS; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_QUICK_SCRUB_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_SWIPE_UP_ONBOARDING_TIP; + +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.systemui.shared.system.MetricsLoggerCompat; + +/** + * This class handles AOSP MetricsLogger function calls and logging around + * quickstep interactions. + */ +@SuppressWarnings("unused") +public class UserEventDispatcherExtension extends UserEventDispatcher { + + private static final String TAG = "UserEventDispatcher"; + + public UserEventDispatcherExtension(Context context) { } + + public void logStateChangeAction(int action, int dir, int srcChildTargetType, + int srcParentContainerType, int dstContainerType, + int pageIndex) { + new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY, + dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER); + super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType, + dstContainerType, pageIndex); + } + + public void logActionTip(int actionType, int viewType) { + LauncherLogProto.Action action = new LauncherLogProto.Action(); + LauncherLogProto.Target target = new LauncherLogProto.Target(); + switch(actionType) { + case VISIBLE: + action.type = LauncherLogProto.Action.Type.TIP; + target.type = LauncherLogProto.Target.Type.CONTAINER; + target.containerType = LauncherLogProto.ContainerType.TIP; + break; + case DISMISS: + action.type = LauncherLogProto.Action.Type.TOUCH; + action.touch = LauncherLogProto.Action.Touch.TAP; + target.type = LauncherLogProto.Target.Type.CONTROL; + target.controlType = CANCEL_TARGET; + break; + default: + Log.e(TAG, "Unexpected action type = " + actionType); + } + + switch(viewType) { + case RECENTS_QUICK_SCRUB_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.QUICK_SCRUB_TEXT; + break; + case RECENTS_SWIPE_UP_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.SWIPE_UP_TEXT; + break; + default: + Log.e(TAG, "Unexpected viewType = " + viewType); + } + LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target); + dispatchUserEvent(event, null); + } +} diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java new file mode 100644 index 0000000000..a654482f97 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.RemoteException; +import android.support.annotation.Nullable; +import android.view.animation.Interpolator; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.utilities.RectFEvaluator; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.function.BiConsumer; + +/** + * Utility class to handle window clip animation + */ +@TargetApi(Build.VERSION_CODES.P) +public class ClipAnimationHelper { + + // The bounds of the source app in device coordinates + private final Rect mSourceStackBounds = new Rect(); + // The insets of the source app + private final Rect mSourceInsets = new Rect(); + // The source app bounds with the source insets applied, in the source app window coordinates + private final RectF mSourceRect = new RectF(); + // The bounds of the task view in launcher window coordinates + private final RectF mTargetRect = new RectF(); + // Set when the final window destination is changed, such as offsetting for quick scrub + private final PointF mTargetOffset = new PointF(); + // The insets to be used for clipping the app window, which can be larger than mSourceInsets + // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In + // app window coordinates. + private final RectF mSourceWindowClipInsets = new RectF(); + + // The bounds of launcher (not including insets) in device coordinates + public final Rect mHomeStackBounds = new Rect(); + + // The clip rect in source app window coordinates + private final Rect mClipRect = new Rect(); + private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); + private final Matrix mTmpMatrix = new Matrix(); + private final RectF mTmpRectF = new RectF(); + + private float mTargetScale = 1f; + private float mOffsetScale = 1f; + private Interpolator mInterpolator = LINEAR; + // We translate y slightly faster than the rest of the animation for quick scrub. + private Interpolator mOffsetYInterpolator = LINEAR; + + // Whether to boost the opening animation target layers, or the closing + private int mBoostModeTargetLayers = -1; + // Wether or not applyTransform has been called yet since prepareAnimation() + private boolean mIsFirstFrame = true; + + private BiConsumer mTaskTransformCallback = + (t, a) -> { }; + + private void updateSourceStack(RemoteAnimationTargetCompat target) { + mSourceInsets.set(target.contentInsets); + mSourceStackBounds.set(target.sourceContainerBounds); + + // TODO: Should sourceContainerBounds already have this offset? + mSourceStackBounds.offsetTo(target.position.x, target.position.y); + + } + + public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { + mHomeStackBounds.set(homeStackBounds); + updateSourceStack(target); + } + + public void updateTargetRect(TransformedRect targetRect) { + mOffsetScale = targetRect.scale; + mSourceRect.set(mSourceInsets.left, mSourceInsets.top, + mSourceStackBounds.width() - mSourceInsets.right, + mSourceStackBounds.height() - mSourceInsets.bottom); + mTargetRect.set(targetRect.rect); + Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale); + mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, + mHomeStackBounds.top - mSourceStackBounds.top); + + // Calculate the clip based on the target rect (since the content insets and the + // launcher insets may differ, so the aspect ratio of the target rect can differ + // from the source rect. The difference between the target rect (scaled to the + // source rect) is the amount to clip on each edge. + RectF scaledTargetRect = new RectF(mTargetRect); + Utilities.scaleRectFAboutCenter(scaledTargetRect, + mSourceRect.width() / mTargetRect.width()); + scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top); + mSourceWindowClipInsets.set( + Math.max(scaledTargetRect.left, 0), + Math.max(scaledTargetRect.top, 0), + Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0), + Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0)); + mSourceRect.set(scaledTargetRect); + } + + public void prepareAnimation(boolean isOpening) { + mIsFirstFrame = true; + mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; + } + + public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress) { + RectF currentRect; + mTmpRectF.set(mTargetRect); + Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale); + float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress); + progress = mInterpolator.getInterpolation(progress); + currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); + + synchronized (mTargetOffset) { + // Stay lined up with the center of the target, since it moves for quick scrub. + currentRect.offset(mTargetOffset.x * mOffsetScale * progress, + mTargetOffset.y * offsetYProgress); + } + + mClipRect.left = (int) (mSourceWindowClipInsets.left * progress); + mClipRect.top = (int) (mSourceWindowClipInsets.top * progress); + mClipRect.right = (int) + (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress)); + mClipRect.bottom = (int) + (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress)); + + TransactionCompat transaction = new TransactionCompat(); + if (mIsFirstFrame) { + RemoteAnimationProvider.prepareTargetsForFirstFrame(targetSet.unfilteredApps, + transaction, mBoostModeTargetLayers); + mIsFirstFrame = false; + } + for (RemoteAnimationTargetCompat app : targetSet.apps) { + if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL); + mTmpMatrix.postTranslate(app.position.x, app.position.y); + transaction.setMatrix(app.leash, mTmpMatrix) + .setWindowCrop(app.leash, mClipRect); + } + + if (app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + transaction.setAlpha(app.leash, 1 - progress); + } + + mTaskTransformCallback.accept(transaction, app); + } + transaction.setEarlyWakeup(); + transaction.apply(); + return currentRect; + } + + public void setTaskTransformCallback + (BiConsumer callback) { + mTaskTransformCallback = callback; + } + + public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) { + synchronized (mTargetOffset) { + mTargetOffset.set(offsetX, offsetY); + } + mTargetScale = scale; + mInterpolator = interpolator; + mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0, + QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { + fromTaskThumbnailView(ttv, rv, null); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, + @Nullable RemoteAnimationTargetCompat target) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); + BaseDragLayer dl = activity.getDragLayer(); + + int[] pos = new int[2]; + dl.getLocationOnScreen(pos); + mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); + mHomeStackBounds.offset(pos[0], pos[1]); + + if (target != null) { + updateSourceStack(target); + } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { + updateStackBoundsToMultiWindowTaskSize(activity); + } else { + mSourceStackBounds.set(mHomeStackBounds); + mSourceInsets.set(activity.getDeviceProfile().getInsets()); + } + + TransformedRect targetRect = new TransformedRect(); + dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect); + updateTargetRect(targetRect); + + if (target == null) { + // Transform the clip relative to the target rect. Only do this in the case where we + // aren't applying the insets to the app windows (where the clip should be in target app + // space) + float scale = mTargetRect.width() / mSourceRect.width(); + mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; + mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; + mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; + mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; + } + } + + private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy != null) { + try { + mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); + return; + } catch (RemoteException e) { + // Use half screen size + } + } + + // Assume that the task size is half screen size (minus the insets and the divider size) + DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + int taskWidth = fullDp.availableWidthPx; + int taskHeight = fullDp.availableHeightPx; + int halfDividerSize = activity.getResources() + .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; + + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + + // Align the task to bottom left/right edge (closer to nav bar). + int left = activity.getDeviceProfile().isSeascape() ? insets.left + : (insets.left + fullDp.availableWidthPx - taskWidth); + mSourceStackBounds.set(0, 0, taskWidth, taskHeight); + mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); + } + + public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) { + RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect); + canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left, + mSourceStackBounds.top - mHomeStackBounds.top); + mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL); + + canvas.concat(mTmpMatrix); + canvas.translate(mTargetRect.left, mTargetRect.top); + + float insetProgress = (1 - progress); + ttv.drawOnCanvas(canvas, + -mSourceWindowClipInsets.left * insetProgress, + -mSourceWindowClipInsets.top * insetProgress, + ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress, + ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress, + ttv.getCornerRadius() * progress); + } + + public RectF getTargetRect() { + return mTargetRect; + } + + public RectF getSourceRect() { + return mSourceRect; + } +} diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java new file mode 100644 index 0000000000..ec9c7eaed9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; + +import java.lang.annotation.Retention; + +public class LayoutUtils { + + private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1; + private static final int MULTI_WINDOW_STRATEGY_DEVICE_PROFILE = 2; + + @Retention(SOURCE) + @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE}) + private @interface MultiWindowStrategy {} + + public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) { + float extraSpace; + if (dp.isVerticalBarLayout()) { + extraSpace = 0; + } else { + extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx; + } + calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect); + } + + public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) { + calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect); + } + + @AnyThread + public static void calculateTaskSize(Context context, DeviceProfile dp, + float extraVerticalSpace, @MultiWindowStrategy int multiWindowStrategy, Rect outRect) { + float taskWidth, taskHeight, paddingHorz; + Resources res = context.getResources(); + Rect insets = dp.getInsets(); + + if (dp.isMultiWindowMode) { + if (multiWindowStrategy == MULTI_WINDOW_STRATEGY_HALF_SCREEN) { + DeviceProfile fullDp = dp.getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + taskWidth = fullDp.availableWidthPx; + taskHeight = fullDp.availableHeightPx; + float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) + / 2; + + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + } else { + // multiWindowStrategy == MULTI_WINDOW_STRATEGY_DEVICE_PROFILE + taskWidth = dp.widthPx; + taskHeight = dp.heightPx; + } + paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space); + } else { + taskWidth = dp.availableWidthPx; + taskHeight = dp.availableHeightPx; + paddingHorz = res.getDimension(dp.isVerticalBarLayout() + ? R.dimen.landscape_task_card_horz_space + : R.dimen.portrait_task_card_horz_space); + } + + float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin); + float paddingVert = res.getDimension(R.dimen.task_card_vert_space); + + // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless + // we override the insets ourselves. + int launcherVisibleWidth = dp.widthPx - insets.left - insets.right; + int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom; + + float availableHeight = launcherVisibleHeight + - topIconMargin - extraVerticalSpace - paddingVert; + float availableWidth = launcherVisibleWidth - paddingHorz; + + float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight); + float outWidth = scale * taskWidth; + float outHeight = scale * taskHeight; + + // Center in the visible space + float x = insets.left + (launcherVisibleWidth - outWidth) / 2; + float y = insets.top + Math.max(topIconMargin, + (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2); + outRect.set(Math.round(x), Math.round(y), + Math.round(x + outWidth), Math.round(y + outHeight)); + } +} diff --git a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java new file mode 100644 index 0000000000..e798d5cbcc --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * Utility class to update multiple values with different interpolators and durations during + * the same animation. + */ +public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener { + + private final ArrayList mAllProperties = new ArrayList<>(); + + @Override + public final void onAnimationUpdate(ValueAnimator animator) { + final float percent = animator.getAnimatedFraction(); + final float currentPlayTime = percent * animator.getDuration(); + + for (int i = mAllProperties.size() - 1; i >= 0; i--) { + FloatProp prop = mAllProperties.get(i); + float time = Math.max(0, currentPlayTime - prop.mDelay); + float newPercent = Math.min(1f, time / prop.mDuration); + newPercent = prop.mInterpolator.getInterpolation(newPercent); + prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent); + } + onUpdate(percent); + } + + public abstract void onUpdate(float percent); + + public final class FloatProp { + + public float value; + + private final float mStart; + private final float mEnd; + private final float mDelay; + private final float mDuration; + private final Interpolator mInterpolator; + + public FloatProp(float start, float end, float delay, float duration, Interpolator i) { + value = mStart = start; + mEnd = end; + mDelay = delay; + mDuration = duration; + mInterpolator = i; + + mAllProperties.add(this); + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java new file mode 100644 index 0000000000..bbf223d1ef --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.os.Handler; + +import com.android.launcher3.LauncherAnimationRunner; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +@FunctionalInterface +public interface RemoteAnimationProvider { + + AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets); + + default ActivityOptions toActivityOptions(Handler handler, long duration) { + LauncherAnimationRunner runner = new LauncherAnimationRunner(handler, + false /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(createWindowAnimation(targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation( + new RemoteAnimationAdapterCompat(runner, duration, 0)); + } + + /** + * Prepares the given {@param targets} for a remote animation, and should be called with the + * transaction from the first frame of animation. + * + * @param boostModeTargets The mode indicating which targets to boost in z-order above other + * targets. + */ + static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets, + TransactionCompat t, int boostModeTargets) { + for (RemoteAnimationTargetCompat target : targets) { + int layer = target.mode == boostModeTargets + ? Integer.MAX_VALUE + : target.prefixOrderIndex; + t.setLayer(target.leash, layer); + t.show(target.leash); + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java new file mode 100644 index 0000000000..04b8be58bf --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.ArrayList; + +/** + * Holds a collection of RemoteAnimationTargets, filtered by different properties. + */ +public class RemoteAnimationTargetSet { + + public final RemoteAnimationTargetCompat[] unfilteredApps; + public final RemoteAnimationTargetCompat[] apps; + + public RemoteAnimationTargetSet(RemoteAnimationTargetCompat[] apps, int targetMode) { + ArrayList filteredApps = new ArrayList<>(); + if (apps != null) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.mode == targetMode) { + filteredApps.add(target); + } + } + } + + this.unfilteredApps = apps; + this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]); + } + + public RemoteAnimationTargetCompat findTask(int taskId) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.taskId == taskId) { + return target; + } + } + return null; + } + + public boolean isAnimatingHome() { + for (RemoteAnimationTargetCompat target : apps) { + if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java new file mode 100644 index 0000000000..40dd74bbb5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static com.android.quickstep.util.RemoteAnimationProvider.prepareTargetsForFirstFrame; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +/** + * Animation listener which fades out the closing targets + */ +public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener { + + private final RemoteAnimationTargetSet mTarget; + private boolean mFirstFrame = true; + + public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] targets) { + mTarget = new RemoteAnimationTargetSet(targets, MODE_CLOSING); + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + TransactionCompat t = new TransactionCompat(); + if (mFirstFrame) { + prepareTargetsForFirstFrame(mTarget.unfilteredApps, t, MODE_CLOSING); + mFirstFrame = false; + } + + float alpha = 1 - valueAnimator.getAnimatedFraction(); + for (RemoteAnimationTargetCompat app : mTarget.apps) { + t.setAlpha(app.leash, alpha); + } + t.apply(); + } +} diff --git a/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java new file mode 100644 index 0000000000..48b07a7144 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.FloatProperty; +import android.view.View; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; + +public class TaskViewDrawable extends Drawable { + + public static final FloatProperty PROGRESS = + new FloatProperty("progress") { + @Override + public void setValue(TaskViewDrawable taskViewDrawable, float v) { + taskViewDrawable.setProgress(v); + } + + @Override + public Float get(TaskViewDrawable taskViewDrawable) { + return taskViewDrawable.mProgress; + } + }; + + /** + * The progress at which we play the atomic icon scale animation. + */ + private static final float ICON_SCALE_THRESHOLD = 0.95f; + + private final RecentsView mParent; + private final View mIconView; + private final int[] mIconPos; + + private final TaskThumbnailView mThumbnailView; + + private final ClipAnimationHelper mClipAnimationHelper; + + private float mProgress = 1; + private boolean mPassedIconScaleThreshold; + private ValueAnimator mIconScaleAnimator; + private float mIconScale; + + public TaskViewDrawable(TaskView tv, RecentsView parent) { + mParent = parent; + mIconView = tv.getIconView(); + mIconPos = new int[2]; + mIconScale = mIconView.getScaleX(); + Utilities.getDescendantCoordRelativeToAncestor(mIconView, parent, mIconPos, true); + + mThumbnailView = tv.getThumbnail(); + mClipAnimationHelper = new ClipAnimationHelper(); + mClipAnimationHelper.fromTaskThumbnailView(mThumbnailView, parent); + } + + public void setProgress(float progress) { + mProgress = progress; + mParent.invalidate(); + boolean passedIconScaleThreshold = progress <= ICON_SCALE_THRESHOLD; + if (mPassedIconScaleThreshold != passedIconScaleThreshold) { + mPassedIconScaleThreshold = passedIconScaleThreshold; + animateIconScale(mPassedIconScaleThreshold ? 0 : 1); + } + } + + private void animateIconScale(float toScale) { + if (mIconScaleAnimator != null) { + mIconScaleAnimator.cancel(); + } + mIconScaleAnimator = ValueAnimator.ofFloat(mIconScale, toScale); + mIconScaleAnimator.addUpdateListener(valueAnimator -> { + mIconScale = (float) valueAnimator.getAnimatedValue(); + if (mProgress > ICON_SCALE_THRESHOLD) { + // Speed up the icon scale to ensure it is 1 when progress is 1. + float iconProgress = (mProgress - ICON_SCALE_THRESHOLD) / (1 - ICON_SCALE_THRESHOLD); + if (iconProgress > mIconScale) { + mIconScale = iconProgress; + } + } + invalidateSelf(); + }); + mIconScaleAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIconScaleAnimator = null; + } + }); + mIconScaleAnimator.setDuration(TaskView.SCALE_ICON_DURATION); + mIconScaleAnimator.start(); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + canvas.translate(mParent.getScrollX(), mParent.getScrollY()); + mClipAnimationHelper.drawForProgress(mThumbnailView, canvas, mProgress); + canvas.restore(); + + canvas.save(); + canvas.translate(mIconPos[0], mIconPos[1]); + canvas.scale(mIconScale, mIconScale, mIconView.getWidth() / 2, mIconView.getHeight() / 2); + mIconView.draw(canvas); + canvas.restore(); + } + + public ClipAnimationHelper getClipAnimationHelper() { + return mClipAnimationHelper; + } + + @Override + public void setAlpha(int i) { } + + @Override + public void setColorFilter(ColorFilter colorFilter) { } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/quickstep/src/com/android/quickstep/util/TransformedRect.java b/quickstep/src/com/android/quickstep/util/TransformedRect.java new file mode 100644 index 0000000000..79f11e405b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TransformedRect.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.graphics.Rect; + +/** + * A wrapper around {@link Rect} with additional transformation properties + */ +public class TransformedRect { + + public final Rect rect = new Rect(); + public float scale = 1; + + public void set(TransformedRect transformedRect) { + rect.set(transformedRect.rect); + scale = transformedRect.scale; + } +} diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java new file mode 100644 index 0000000000..0025df136d --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import android.content.Context; +import android.graphics.Rect; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; + +public class ClearAllButton extends Button { + RecentsView mRecentsView; + + public ClearAllButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public void setRecentsView(RecentsView recentsView) { + mRecentsView = recentsView; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setParent(mRecentsView); // Pretend we are a part of the task carousel. + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + if (focused) { + mRecentsView.revealClearAllButton(); + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java new file mode 100644 index 0000000000..c359966df4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +public class IconView extends View { + + private Drawable mDrawable; + + public IconView(Context context) { + super(context); + } + + public IconView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setDrawable(Drawable d) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + mDrawable = d; + if (mDrawable != null) { + mDrawable.setCallback(this); + mDrawable.setBounds(0, 0, getWidth(), getHeight()); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mDrawable != null) { + mDrawable.setBounds(0, 0, w, h); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + final Drawable drawable = mDrawable; + if (drawable != null && drawable.isStateful() + && drawable.setState(getDrawableState())) { + invalidateDrawable(drawable); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDrawable != null) { + mDrawable.draw(canvas); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java new file mode 100644 index 0000000000..c149de54f0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views; + +import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK; +import static com.android.launcher3.states.RotationHelper.REQUEST_NONE; + +import android.graphics.Rect; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Insettable; +import com.android.launcher3.Launcher; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.WindowTransformSwipeHandler; + +/** + * Floating view which shows the task snapshot allowing it to be dragged and placed. + */ +public class LauncherLayoutListener extends AbstractFloatingView + implements Insettable, LayoutListener { + + private final Launcher mLauncher; + private WindowTransformSwipeHandler mHandler; + + public LauncherLayoutListener(Launcher launcher) { + super(launcher, null); + mLauncher = launcher; + setVisibility(INVISIBLE); + + // For the duration of the gesture, lock the screen orientation to ensure that we do not + // rotate mid-quickscrub + launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK); + } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { + mHandler = handler; + } + + @Override + public void setInsets(Rect insets) { + if (mHandler != null) { + mHandler.buildAnimationController(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (mIsOpen) { + mIsOpen = false; + // We don't support animate. + mLauncher.getDragLayer().removeView(this); + + if (mHandler != null) { + mHandler.layoutListenerClosed(); + } + } + } + + @Override + public void open() { + if (!mIsOpen) { + mLauncher.getDragLayer().addView(this); + mIsOpen = true; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(1, 1); + } + + @Override + public void logActionCommand(int command) { + // We should probably log the weather + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_QUICKSTEP_PREVIEW) != 0; + } + + @Override + public void finish() { + setHandler(null); + close(false); + mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE); + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java new file mode 100644 index 0000000000..5aca4f3263 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views; + +import static com.android.launcher3.LauncherAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN; +import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.view.View; +import android.view.ViewDebug; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.ScrimView; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.LayoutUtils; + +/** + * {@link RecentsView} used in Launcher activity + */ +@TargetApi(Build.VERSION_CODES.O) +public class LauncherRecentsView extends RecentsView { + + public static final FloatProperty TRANSLATION_Y_FACTOR = + new FloatProperty("translationYFactor") { + + @Override + public void setValue(LauncherRecentsView view, float v) { + view.setTranslationYFactor(v); + } + + @Override + public Float get(LauncherRecentsView view) { + return view.mTranslationYFactor; + } + }; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mTranslationYFactor; + + public LauncherRecentsView(Context context) { + this(context, null); + } + + public LauncherRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setContentAlpha(0); + } + + @Override + protected void onAllTasksRemoved() { + mActivity.getStateManager().goToState(NORMAL); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setTranslationYFactor(mTranslationYFactor); + } + + public void setTranslationYFactor(float translationFactor) { + mTranslationYFactor = translationFactor; + setTranslationY(computeTranslationYForFactor(mTranslationYFactor)); + } + + public float computeTranslationYForFactor(float translationYFactor) { + return translationYFactor * (getPaddingBottom() - getPaddingTop()); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + protected void onTaskStackUpdated() { + // Lazily update the empty message only when the task stack is reapplied + updateEmptyMessage(); + } + + /** + * Animates adjacent tasks and translate hotseat off screen as well. + */ + @Override + public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv, + ClipAnimationHelper helper) { + AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper); + + if (!OverviewInteractionState.getInstance(mActivity).isSwipeUpGestureEnabled()) { + // Hotseat doesn't move when opening recents with the button, + // so don't animate it here either. + return anim; + } + + float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN; + LauncherState state = mActivity.getStateManager().getState(); + if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) { + float maxShiftRange = mActivity.getDeviceProfile().heightPx; + float currShiftRange = mActivity.getAllAppsController().getShiftRange(); + allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange; + } + anim.play(ObjectAnimator.ofFloat( + mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen)); + + ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt( + mActivity.findViewById(R.id.scrim_view), ScrimView.DRAG_HANDLE_ALPHA, 0); + dragHandleAnim.setInterpolator(Interpolators.ACCEL_2); + anim.play(dragHandleAnim); + + return anim; + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect); + } + + @Override + protected void onTaskLaunched(boolean success) { + if (success) { + mActivity.getStateManager().goToState(NORMAL, false /* animate */); + } else { + LauncherState state = mActivity.getStateManager().getState(); + mActivity.getAllAppsController().setState(state); + } + super.onTaskLaunched(success); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + return mActivity.isInMultiWindowModeCompat(); + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java new file mode 100644 index 0000000000..d550edcd33 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -0,0 +1,1373 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.support.annotation.Nullable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.SparseBooleanArray; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ListView; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Insettable; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.PropertyListBuilder; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.Themes; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.TaskViewDrawable; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.TaskStack; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.function.Consumer; + +/** + * A list of recent tasks. + */ +@TargetApi(Build.VERSION_CODES.P) +public abstract class RecentsView extends PagedView implements Insettable { + + private static final String TAG = RecentsView.class.getSimpleName(); + + private final Rect mTempRect = new Rect(); + + private static final int DISMISS_TASK_DURATION = 300; + // The threshold at which we update the SystemUI flags when animating from the task into the app + private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.6f; + + private static final float[] sTempFloatArray = new float[3]; + + protected final T mActivity; + private final QuickScrubController mQuickScrubController; + private final float mFastFlingVelocity; + private final RecentsModel mModel; + private final int mTaskTopMargin; + + private final ScrollState mScrollState = new ScrollState(); + // Keeps track of the previously known visible tasks for purposes of loading/unloading task data + private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); + + private boolean mIsClearAllButtonFullyRevealed; + + /** + * TODO: Call reloadIdNeeded in onTaskStackChanged. + */ + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { + if (!mHandleTaskStackChanges) { + return; + } + updateThumbnail(taskId, snapshot); + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + if (!mHandleTaskStackChanges) { + return; + } + // Check this is for the right user + if (!checkCurrentOrManagedUserId(userId, getContext())) { + return; + } + + // Remove the task immediately from the task list + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + removeView(taskView); + } + } + + @Override + public void onActivityUnpinned() { + if (!mHandleTaskStackChanges) { + return; + } + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + + @Override + public void onTaskRemoved(int taskId) { + if (!mHandleTaskStackChanges) { + return; + } + BackgroundExecutor.get().submit(() -> { + TaskView taskView = getTaskView(taskId); + if (taskView == null) { + return; + } + Handler handler = taskView.getHandler(); + if (handler == null) { + return; + } + + // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and + // remove all these checks + Task.TaskKey taskKey = taskView.getTask().key; + if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), + taskKey.userId) == null) { + // The package was uninstalled + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } else { + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext()); + RecentsTaskLoadPlan.PreloadOptions opts = + new RecentsTaskLoadPlan.PreloadOptions(); + opts.loadTitles = false; + loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1, + UserHandle.myUserId()); + if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) { + // The task was removed from the recents list + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } + } + }); + } + + @Override + public void onPinnedStackAnimationStarted() { + // Needed for activities that auto-enter PiP, which will not trigger a remote + // animation to be created + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } + }; + + private int mLoadPlanId = -1; + + // Only valid until the launcher state changes to NORMAL + private int mRunningTaskId = -1; + private boolean mRunningTaskTileHidden; + private Task mTmpRunningTask; + + private boolean mRunningTaskIconScaledDown = false; + + private boolean mOverviewStateEnabled; + private boolean mHandleTaskStackChanges; + private Runnable mNextPageSwitchRunnable; + private boolean mSwipeDownShouldLaunchApp; + + private PendingAnimation mPendingAnimation; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mContentAlpha = 1; + + // Keeps track of task views whose visual state should not be reset + private ArraySet mIgnoreResetTaskViews = new ArraySet<>(); + + private View mClearAllButton; + + // Variables for empty state + private final Drawable mEmptyIcon; + private final CharSequence mEmptyMessage; + private final TextPaint mEmptyMessagePaint; + private final Point mLastMeasureSize = new Point(); + private final int mEmptyMessagePadding; + private boolean mShowEmptyMessage; + private Layout mEmptyTextLayout; + + private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = + (inMultiWindowMode) -> { + if (!inMultiWindowMode && mOverviewStateEnabled) { + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + }; + + public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); + enableFreeScroll(true); + setClipToOutline(true); + + mFastFlingVelocity = getResources() + .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); + mActivity = (T) BaseActivity.fromContext(context); + mQuickScrubController = new QuickScrubController(mActivity, this); + mModel = RecentsModel.getInstance(context); + + mIsRtl = !Utilities.isRtl(getResources()); + setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); + mTaskTopMargin = getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + + mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); + mEmptyIcon.setCallback(this); + mEmptyMessage = context.getText(R.string.recents_empty_message); + mEmptyMessagePaint = new TextPaint(); + mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); + mEmptyMessagePaint.setTextSize(getResources() + .getDimension(R.dimen.recents_empty_message_text_size)); + mEmptyMessagePadding = getResources() + .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); + setWillNotDraw(false); + updateEmptyMessage(); + setFocusable(false); + } + + public boolean isRtl() { + return mIsRtl; + } + + public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); + } + return taskView; + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + updateTaskStackListenerState(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateTaskStackListenerState(); + mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + updateTaskStackListenerState(); + mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + + // Clear the task data for the removed child if it was visible + Task task = ((TaskView) child).getTask(); + if (mHasVisibleTaskData.get(task.key.id)) { + mHasVisibleTaskData.delete(task.key.id); + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + onChildViewsChanged(); + } + + public boolean isTaskViewVisible(TaskView tv) { + // For now, just check if it's the active task or an adjacent task + return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; + } + + public TaskView getTaskView(int taskId) { + for (int i = 0; i < getChildCount(); i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask().key.id == taskId) { + return tv; + } + } + return null; + } + + public void setOverviewStateEnabled(boolean enabled) { + mOverviewStateEnabled = enabled; + updateTaskStackListenerState(); + } + + public void setNextPageSwitchRunnable(Runnable r) { + mNextPageSwitchRunnable = r; + } + + @Override + protected void onPageEndTransition() { + super.onPageEndTransition(); + if (mNextPageSwitchRunnable != null) { + mNextPageSwitchRunnable.run(); + mNextPageSwitchRunnable = null; + } + if (getNextPage() > 0) { + setSwipeDownShouldLaunchApp(true); + } + } + + private int getScrollEnd() { + return mIsRtl ? 0 : mMaxScrollX; + } + + private float calculateClearAllButtonAlpha() { + final int childCount = getChildCount(); + if (mShowEmptyMessage || childCount == 0 || mPageScrolls == null + || childCount != mPageScrolls.length) { + return 0; + } + + final int scrollEnd = getScrollEnd(); + final int oldestChildScroll = getScrollForPage(childCount - 1); + + final int clearAllButtonMotionRange = scrollEnd - oldestChildScroll; + if (clearAllButtonMotionRange == 0) return 0; + + final float alphaUnbound = ((float) (getScrollX() - oldestChildScroll)) / + clearAllButtonMotionRange; + if (alphaUnbound > 1) return 0; + + return Math.max(alphaUnbound, 0); + } + + private void updateClearAllButtonAlpha() { + if (mClearAllButton != null) { + final float alpha = calculateClearAllButtonAlpha(); + final boolean revealed = alpha == 1; + if (mIsClearAllButtonFullyRevealed != revealed) { + mIsClearAllButtonFullyRevealed = revealed; + mClearAllButton.setImportantForAccessibility(revealed ? + IMPORTANT_FOR_ACCESSIBILITY_YES : + IMPORTANT_FOR_ACCESSIBILITY_NO); + } + mClearAllButton.setAlpha(alpha * mContentAlpha); + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + updateClearAllButtonAlpha(); + } + + @Override + protected void restoreScrollOnLayout() { + if (mIsClearAllButtonFullyRevealed) { + scrollAndForceFinish(getScrollEnd()); + } else { + super.restoreScrollOnLayout(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST + && mScroller.isFinished() && mIsClearAllButtonFullyRevealed) { + mClearAllButton.getHitRect(mTempRect); + mTempRect.offset(-getLeft(), -getTop()); + if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) { + // If nothing is in motion, let the Clear All button process the event. + return false; + } + } + + if (ev.getAction() == MotionEvent.ACTION_UP && mShowEmptyMessage) { + onAllTasksRemoved(); + } + return super.onTouchEvent(ev); + } + + private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { + if (mPendingAnimation != null) { + mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan)); + return; + } + TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; + if (stack == null) { + removeAllViews(); + onTaskStackUpdated(); + return; + } + + int oldChildCount = getChildCount(); + + // Ensure there are as many views as there are tasks in the stack (adding and trimming as + // necessary) + final LayoutInflater inflater = LayoutInflater.from(getContext()); + final ArrayList tasks = new ArrayList<>(stack.getTasks()); + + final int requiredChildCount = tasks.size(); + for (int i = getChildCount(); i < requiredChildCount; i++) { + final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false); + addView(taskView); + } + while (getChildCount() > requiredChildCount) { + final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1); + removeView(taskView); + } + + // Unload existing visible task data + unloadVisibleTaskData(); + + // Rebind and reset all task views + for (int i = requiredChildCount - 1; i >= 0; i--) { + final int pageIndex = requiredChildCount - i - 1; + final Task task = tasks.get(i); + final TaskView taskView = (TaskView) getChildAt(pageIndex); + taskView.bind(task); + } + resetTaskVisuals(); + + if (oldChildCount != getChildCount()) { + mQuickScrubController.snapToNextTaskIfAvailable(); + } + onTaskStackUpdated(); + } + + protected void onTaskStackUpdated() { } + + public void resetTaskVisuals() { + for (int i = getChildCount() - 1; i >= 0; i--) { + TaskView taskView = (TaskView) getChildAt(i); + if (!mIgnoreResetTaskViews.contains(taskView)) { + taskView.resetVisualProperties(); + } + } + if (mRunningTaskTileHidden) { + setRunningTaskHidden(mRunningTaskTileHidden); + } + applyIconScale(false /* animate */); + + updateCurveProperties(); + // Update the set of visible task's data + loadVisibleTaskData(); + } + + private void updateTaskStackListenerState() { + boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() + && getWindowVisibility() == VISIBLE; + if (handleTaskStackChanges != mHandleTaskStackChanges) { + mHandleTaskStackChanges = handleTaskStackChanges; + if (handleTaskStackChanges) { + reloadIfNeeded(); + } + } + } + + @Override + public void setInsets(Rect insets) { + mInsets.set(insets); + DeviceProfile dp = mActivity.getDeviceProfile(); + getTaskSize(dp, mTempRect); + + // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub. + mTempRect.top -= mTaskTopMargin; + setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, + dp.availableWidthPx + mInsets.left - mTempRect.right, + dp.availableHeightPx + mInsets.top - mTempRect.bottom); + } + + protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); + + public void getTaskSize(Rect outRect) { + getTaskSize(mActivity.getDeviceProfile(), outRect); + } + + @Override + protected boolean computeScrollHelper() { + boolean scrolling = super.computeScrollHelper(); + boolean isFlingingFast = false; + updateCurveProperties(); + if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) { + if (scrolling) { + // Check if we are flinging quickly to disable high res thumbnail loading + isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; + } + + // After scrolling, update the visible task's data + loadVisibleTaskData(); + } + + // Update the high res thumbnail loader + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast); + return scrolling; + } + + /** + * Scales and adjusts translation of adjacent pages as if on a curved carousel. + */ + public void updateCurveProperties() { + if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { + return; + } + final int halfPageWidth = getNormalChildWidth() / 2; + final int screenCenter = mInsets.left + getPaddingLeft() + getScrollX() + halfPageWidth; + final int halfScreenWidth = getMeasuredWidth() / 2; + final int pageSpacing = mPageSpacing; + + final int pageCount = getPageCount(); + for (int i = 0; i < pageCount; i++) { + View page = getPageAt(i); + float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; + float distanceFromScreenCenter = screenCenter - pageCenter; + float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; + mScrollState.linearInterpolation = Math.min(1, + Math.abs(distanceFromScreenCenter) / distanceToReachEdge); + ((PageCallbacks) page).onPageScroll(mScrollState); + } + } + + /** + * Iterates through all thet asks, and loads the associated task data for newly visible tasks, + * and unloads the associated task data for tasks that are no longer visible. + */ + public void loadVisibleTaskData() { + if (!mOverviewStateEnabled) { + // Skip loading visible task data if we've already left the overview state + return; + } + + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + int centerPageIndex = getPageNearestToCenterOfScreen(); + int lower = Math.max(0, centerPageIndex - 2); + int upper = Math.min(centerPageIndex + 2, getChildCount() - 1); + int numChildren = getChildCount(); + + // Update the task data for the in/visible children + for (int i = 0; i < numChildren; i++) { + TaskView taskView = (TaskView) getChildAt(i); + Task task = taskView.getTask(); + boolean visible = lower <= i && i <= upper; + if (visible) { + if (task == mTmpRunningTask) { + // Skip loading if this is the task that we are animating into + continue; + } + if (!mHasVisibleTaskData.get(task.key.id)) { + loader.loadTaskData(task); + loader.getHighResThumbnailLoader().onTaskVisible(task); + } + mHasVisibleTaskData.put(task.key.id, visible); + } else { + if (mHasVisibleTaskData.get(task.key.id)) { + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + mHasVisibleTaskData.delete(task.key.id); + } + } + } + + /** + * Unloads any associated data from the currently visible tasks + */ + private void unloadVisibleTaskData() { + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + for (int i = 0; i < mHasVisibleTaskData.size(); i++) { + if (mHasVisibleTaskData.valueAt(i)) { + TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); + Task task = taskView.getTask(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + } + mHasVisibleTaskData.clear(); + } + + protected abstract void onAllTasksRemoved(); + + public void reset() { + mRunningTaskId = -1; + mRunningTaskTileHidden = false; + + unloadVisibleTaskData(); + setCurrentPage(0); + + OverviewCallbacks.get(getContext()).onResetOverview(); + } + + /** + * Reloads the view if anything in recents changed. + */ + public void reloadIfNeeded() { + if (!mModel.isLoadPlanValid(mLoadPlanId)) { + mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); + } + } + + /** + * Ensures that the first task in the view represents {@param task} and reloads the view + * if needed. This allows the swipe-up gesture to assume that the first tile always + * corresponds to the correct task. + * All subsequent calls to reload will keep the task as the first item until {@link #reset()} + * is called. + * Also scrolls the view to this task + */ + public void showTask(int runningTaskId) { + if (getChildCount() == 0) { + // Add an empty view for now until the task plan is loaded and applied + final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) + .inflate(R.layout.task, this, false); + addView(taskView); + + // The temporary running task is only used for the duration between the start of the + // gesture and the task list is loaded and applied + mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), 0, 0), null, + null, "", "", 0, 0, false, true, false, false, + new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false); + taskView.bind(mTmpRunningTask); + } + setCurrentTask(runningTaskId); + } + + /** + * Hides the tile associated with {@link #mRunningTaskId} + */ + public void setRunningTaskHidden(boolean isHidden) { + mRunningTaskTileHidden = isHidden; + TaskView runningTask = getTaskView(mRunningTaskId); + if (runningTask != null) { + runningTask.setAlpha(isHidden ? 0 : mContentAlpha); + } + } + + /** + * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile. + */ + public void setCurrentTask(int runningTaskId) { + boolean runningTaskTileHidden = mRunningTaskTileHidden; + boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown; + + setRunningTaskIconScaledDown(false, false); + setRunningTaskHidden(false); + mRunningTaskId = runningTaskId; + setRunningTaskIconScaledDown(runningTaskIconScaledDown, false); + setRunningTaskHidden(runningTaskTileHidden); + + setCurrentPage(0); + + // Load the tasks (if the loading is already + mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan); + } + + public void showNextTask() { + TaskView runningTaskView = getTaskView(mRunningTaskId); + if (runningTaskView == null) { + // Launch the first task + if (getChildCount() > 0) { + ((TaskView) getChildAt(0)).launchTask(true /* animate */); + } + } else { + // Get the next launch task + int runningTaskIndex = indexOfChild(runningTaskView); + int nextTaskIndex = Math.max(0, Math.min(getChildCount() - 1, runningTaskIndex + 1)); + if (nextTaskIndex < getChildCount()) { + ((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */); + } + } + } + + public QuickScrubController getQuickScrubController() { + return mQuickScrubController; + } + + public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) { + if (mRunningTaskIconScaledDown == isScaledDown) { + return; + } + mRunningTaskIconScaledDown = isScaledDown; + applyIconScale(animate); + } + + private void applyIconScale(boolean animate) { + float scale = mRunningTaskIconScaledDown ? 0 : 1; + TaskView firstTask = getTaskView(mRunningTaskId); + if (firstTask != null) { + if (animate) { + firstTask.animateIconToScaleAndDim(scale); + } else { + firstTask.setIconScaleAndDim(scale); + } + } + } + + public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { + mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; + } + + public boolean shouldSwipeDownLaunchApp() { + return mSwipeDownShouldLaunchApp; + } + + public interface PageCallbacks { + + /** + * Updates the page UI based on scroll params. + */ + default void onPageScroll(ScrollState scrollState) {}; + } + + public static class ScrollState { + + /** + * The progress from 0 to 1, where 0 is the center + * of the screen and 1 is the edge of the screen. + */ + public float linearInterpolation; + } + + public void addIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.add(taskView); + } + + public void removeIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.remove(taskView); + } + + private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { + addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); + addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), + duration, LINEAR, anim); + } + + private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, + boolean shouldLog) { + if (task != null) { + ActivityManagerWrapper.getInstance().removeTask(task.key.id); + if (shouldLog) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.UP, index, + TaskUtils.getComponentKeyForTask(task.key)); + } + } + } + + public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, + boolean shouldRemoveTask, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getChildCount(); + if (count == 0) { + return pendingAnimation; + } + + int[] oldScroll = new int[count]; + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + + int[] newScroll = new int[count]; + getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); + + int scrollDiffPerPage = 0; + int leftmostPage = mIsRtl ? count -1 : 0; + int rightmostPage = mIsRtl ? 0 : count - 1; + if (count > 1) { + int secondRightmostPage = mIsRtl ? 1 : count - 2; + scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage]; + } + int draggedIndex = indexOfChild(taskView); + + boolean needsCurveUpdates = false; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child == taskView) { + if (animateTaskView) { + addDismissedTaskAnimations(taskView, anim, duration); + } + } else { + // If we just take newScroll - oldScroll, everything to the right of dragged task + // translates to the left. We need to offset this in some cases: + // - In RTL, add page offset to all pages, since we want pages to move to the right + // Additionally, add a page offset if: + // - Current page is rightmost page (leftmost for RTL) + // - Dragging an adjacent page on the left side (right side for RTL) + int offset = mIsRtl ? scrollDiffPerPage : 0; + if (mCurrentPage == draggedIndex) { + int lastPage = mIsRtl ? leftmostPage : rightmostPage; + if (mCurrentPage == lastPage) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } else { + // Dragging an adjacent page. + int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) + if (draggedIndex == negativeAdjacent) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } + int scrollDiff = newScroll[i] - oldScroll[i] + offset; + if (scrollDiff != 0) { + addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), + duration, ACCEL, anim); + needsCurveUpdates = true; + } + } + } + + if (needsCurveUpdates) { + ValueAnimator va = ValueAnimator.ofFloat(0, 1); + va.addUpdateListener((a) -> updateCurveProperties()); + anim.play(va); + } + + // Add a tiny bit of translation Z, so that it draws on top of other views + if (animateTaskView) { + taskView.setTranslationZ(0.1f); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + if (shouldRemoveTask) { + removeTask(taskView.getTask(), draggedIndex, onEndListener, true); + } + int pageToSnapTo = mCurrentPage; + if (draggedIndex < pageToSnapTo) { + pageToSnapTo -= 1; + } + removeView(taskView); + if (getChildCount() == 0) { + onAllTasksRemoved(); + } else if (!mIsClearAllButtonFullyRevealed) { + snapToPageImmediately(pageToSnapTo); + } + } + resetTaskVisuals(); + mPendingAnimation = null; + }); + return pendingAnimation; + } + + public PendingAnimation createAllTasksDismissAnimation(long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getChildCount(); + for (int i = 0; i < count; i++) { + addDismissedTaskAnimations(getChildAt(i), anim, duration); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + while (getChildCount() != 0) { + TaskView taskView = getPageAt(getChildCount() - 1); + removeTask(taskView.getTask(), -1, onEndListener, false); + removeView(taskView); + } + onAllTasksRemoved(); + } + mPendingAnimation = null; + }); + return pendingAnimation; + } + + private static void addAnim(ObjectAnimator anim, long duration, + TimeInterpolator interpolator, AnimatorSet set) { + anim.setDuration(duration).setInterpolator(interpolator); + set.play(anim); + } + + private boolean snapToPageRelative(int delta, boolean cycle) { + if (getPageCount() == 0) { + return false; + } + final int newPageUnbound = getNextPage() + delta; + if (!cycle && (newPageUnbound < 0 || newPageUnbound >= getChildCount())) { + return false; + } + snapToPage((newPageUnbound + getPageCount()) % getPageCount()); + return true; + } + + private void runDismissAnimation(PendingAnimation pendingAnim) { + AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( + pendingAnim.anim, DISMISS_TASK_DURATION); + controller.dispatchOnStart(); + controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); + controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); + controller.start(); + } + + public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { + runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, + DISMISS_TASK_DURATION)); + } + + public void dismissAllTasks() { + runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_TAB: + return snapToPageRelative(event.isShiftPressed() ? -1 : 1, + event.isAltPressed() /* cycle */); + case KeyEvent.KEYCODE_DPAD_RIGHT: + return snapToPageRelative(mIsRtl ? -1 : 1, false /* cycle */); + case KeyEvent.KEYCODE_DPAD_LEFT: + return snapToPageRelative(mIsRtl ? 1 : -1, false /* cycle */); + case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_FORWARD_DEL: + dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/, + true /*removeTask*/); + return true; + case KeyEvent.KEYCODE_NUMPAD_DOT: + if (event.isAltPressed()) { + // Numpad DEL pressed while holding Alt. + dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/, + true /*removeTask*/); + return true; + } + } + } + return super.dispatchKeyEvent(event); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, + @Nullable Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (gainFocus && getChildCount() > 0) { + switch (direction) { + case FOCUS_FORWARD: + setCurrentPage(0); + break; + case FOCUS_BACKWARD: + case FOCUS_RIGHT: + case FOCUS_LEFT: + setCurrentPage(getChildCount() - 1); + break; + } + } + } + + public float getContentAlpha() { + return mContentAlpha; + } + + public void setContentAlpha(float alpha) { + alpha = Utilities.boundToRange(alpha, 0, 1); + mContentAlpha = alpha; + for (int i = getChildCount() - 1; i >= 0; i--) { + TaskView child = getPageAt(i); + if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { + getChildAt(i).setAlpha(alpha); + } + } + + int alphaInt = Math.round(alpha * 255); + mEmptyMessagePaint.setAlpha(alphaInt); + mEmptyIcon.setAlpha(alphaInt); + updateClearAllButtonAlpha(); + } + + private float[] getAdjacentScaleAndTranslation(TaskView currTask, + float currTaskToScale, float currTaskToTranslationY) { + float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale()); + sTempFloatArray[0] = currTaskToScale; + sTempFloatArray[1] = mIsRtl ? -displacement : displacement; + sTempFloatArray[2] = currTaskToTranslationY; + return sTempFloatArray; + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + child.setAlpha(mContentAlpha); + onChildViewsChanged(); + } + + @Override + public TaskView getPageAt(int index) { + return (TaskView) getChildAt(index); + } + + public void updateEmptyMessage() { + boolean isEmpty = getChildCount() == 0; + boolean hasSizeChanged = mLastMeasureSize.x != getWidth() + || mLastMeasureSize.y != getHeight(); + if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { + return; + } + setContentDescription(isEmpty ? mEmptyMessage : ""); + mShowEmptyMessage = isEmpty; + updateEmptyStateUi(hasSizeChanged); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateEmptyStateUi(changed); + + // Set the pivot points to match the task preview center + setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) + + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); + setPivotX(((mInsets.left + getPaddingLeft()) + + (getWidth() - mInsets.right - getPaddingRight())) / 2); + } + + private void updateEmptyStateUi(boolean sizeChanged) { + boolean hasValidSize = getWidth() > 0 && getHeight() > 0; + if (sizeChanged && hasValidSize) { + mEmptyTextLayout = null; + mLastMeasureSize.set(getWidth(), getHeight()); + } + updateClearAllButtonAlpha(); + + if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { + int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; + mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), + mEmptyMessagePaint, availableWidth) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .build(); + int totalHeight = mEmptyTextLayout.getHeight() + + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); + + int top = (mLastMeasureSize.y - totalHeight) / 2; + int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; + mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), + top + mEmptyIcon.getIntrinsicHeight()); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); + } + + protected void maybeDrawEmptyMessage(Canvas canvas) { + if (mShowEmptyMessage && mEmptyTextLayout != null) { + // Offset to center in the visible (non-padded) part of RecentsView + mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), + mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); + canvas.save(); + canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, + (mTempRect.top - mTempRect.bottom) / 2); + mEmptyIcon.draw(canvas); + canvas.translate(mEmptyMessagePadding, + mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); + mEmptyTextLayout.draw(canvas); + canvas.restore(); + } + } + + /** + * Animate adjacent tasks off screen while scaling up. + * + * If launching one of the adjacent tasks, parallax the center task and other adjacent task + * to the right. + */ + public AnimatorSet createAdjacentPageAnimForTaskLaunch( + TaskView tv, ClipAnimationHelper clipAnimationHelper) { + AnimatorSet anim = new AnimatorSet(); + + int taskIndex = indexOfChild(tv); + int centerTaskIndex = getCurrentPage(); + boolean launchingCenterTask = taskIndex == centerTaskIndex; + + float toScale = clipAnimationHelper.getSourceRect().width() + / clipAnimationHelper.getTargetRect().width(); + float toTranslationY = clipAnimationHelper.getSourceRect().centerY() + - clipAnimationHelper.getTargetRect().centerY(); + if (launchingCenterTask) { + TaskView centerTask = getPageAt(centerTaskIndex); + if (taskIndex - 1 >= 0) { + TaskView adjacentTask = getPageAt(taskIndex - 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + scaleAndTranslation[1] = -scaleAndTranslation[1]; + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + if (taskIndex + 1 < getPageCount()) { + TaskView adjacentTask = getPageAt(taskIndex + 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + } else { + // We are launching an adjacent task, so parallax the center and other adjacent task. + float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); + anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, + mIsRtl ? -displacementX : displacementX)); + + int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); + if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { + anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), + new PropertyListBuilder() + .translationX(mIsRtl ? -displacementX : displacementX) + .scale(1) + .build())); + } + } + return anim; + } + + private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) { + AnimatorSet anim = new AnimatorSet(); + anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0])); + anim.play(ObjectAnimator.ofPropertyValuesHolder(child, + new PropertyListBuilder() + .translationX(toScaleAndTranslation[1]) + .translationY(toScaleAndTranslation[2]) + .build())); + return anim; + } + + public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + + int count = getChildCount(); + if (count == 0) { + return new PendingAnimation(new AnimatorSet()); + } + + tv.setVisibility(INVISIBLE); + int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); + TaskViewDrawable drawable = new TaskViewDrawable(tv, this); + getOverlay().add(drawable); + + ObjectAnimator drawableAnim = + ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0); + drawableAnim.setInterpolator(LINEAR); + drawableAnim.addUpdateListener((animator) -> { + // Once we pass a certain threshold, update the sysui flags to match the target tasks' + // flags + mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, + animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD + ? targetSysUiFlags + : 0); + }); + + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, + drawable.getClipAnimationHelper()); + anim.play(drawableAnim); + anim.setDuration(duration); + + Consumer onTaskLaunchFinish = (result) -> { + onTaskLaunched(result); + tv.setVisibility(VISIBLE); + getOverlay().remove(drawable); + }; + + mPendingAnimation = new PendingAnimation(anim); + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + Consumer onLaunchResult = (result) -> { + onTaskLaunchFinish.accept(result); + if (!result) { + tv.notifyTaskLaunchFailed(TAG); + } + }; + tv.launchTask(false, onLaunchResult, getHandler()); + Task task = tv.getTask(); + if (task != null) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.DOWN, indexOfChild(tv), + TaskUtils.getComponentKeyForTask(task.key)); + } + } else { + onTaskLaunchFinish.accept(false); + } + mPendingAnimation = null; + }); + return mPendingAnimation; + } + + public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); + + protected void onTaskLaunched(boolean success) { + resetTaskVisuals(); + } + + @Override + protected void notifyPageSwitchListener(int prevPage) { + super.notifyPageSwitchListener(prevPage); + loadVisibleTaskData(); + } + + @Override + protected String getCurrentPageDescription() { + return ""; + } + + private int additionalScrollForClearAllButton() { + return (int) getResources().getDimension( + R.dimen.clear_all_container_width) - getPaddingEnd(); + } + + @Override + protected int computeMaxScrollX() { + if (getChildCount() == 0) { + return super.computeMaxScrollX(); + } + + // Allow a clear_all_container_width-sized gap after the last task. + return super.computeMaxScrollX() + (mIsRtl ? 0 : additionalScrollForClearAllButton()); + } + + @Override + protected int offsetForPageScrolls() { + return mIsRtl ? additionalScrollForClearAllButton() : 0; + } + + public void setClearAllButton(View clearAllButton) { + mClearAllButton = clearAllButton; + updateClearAllButtonAlpha(); + } + + private void onChildViewsChanged() { + final int childCount = getChildCount(); + mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE); + setFocusable(childCount != 0); + } + + public void revealClearAllButton() { + setCurrentPage(getChildCount() - 1); // Loads tasks info if needed. + scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (getChildCount() > 0) { + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (!mIsClearAllButtonFullyRevealed && getCurrentPage() == getPageCount() - 1) { + revealClearAllButton(); + return true; + } + } + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (mIsClearAllButtonFullyRevealed) { + setCurrentPage(getChildCount() - 1); + return true; + } + } + break; + } + } + return super.performAccessibilityAction(action, arguments); + } + + @Override + public void addChildrenForAccessibility(ArrayList outChildren) { + outChildren.add(mClearAllButton); + for (int i = getChildCount() - 1; i >= 0; --i) { + outChildren.add(getChildAt(i)); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + if (getChildCount() > 0) { + info.addAction(mIsClearAllButtonFullyRevealed ? + AccessibilityNodeInfo.ACTION_SCROLL_FORWARD : + AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); + } + + final AccessibilityNodeInfo.CollectionInfo + collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( + 1, getChildCount(), false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); + info.setCollectionInfo(collectionInfo); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + event.setScrollable(getPageCount() > 0); + + if (!mIsClearAllButtonFullyRevealed + && event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + final int childCount = getChildCount(); + final int[] visibleTasks = getVisibleChildrenRange(); + event.setFromIndex(childCount - visibleTasks[1] - 1); + event.setToIndex(childCount - visibleTasks[0] - 1); + event.setItemCount(childCount); + } + } + + @Override + public CharSequence getAccessibilityClassName() { + // To hear position-in-list related feedback from Talkback. + return ListView.class.getName(); + } + + @Override + protected boolean isPageOrderFlipped() { + return true; + } + + public boolean performTaskAccessibilityActionExtra(int action) { + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java new file mode 100644 index 0000000000..c6cd52769e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.view.MotionEvent; +import android.view.View; + +import com.android.launcher3.InsettableFrameLayout; +import com.android.launcher3.R; + +import java.util.ArrayList; + +public class RecentsViewContainer extends InsettableFrameLayout { + public static final FloatProperty CONTENT_ALPHA = + new FloatProperty("contentAlpha") { + @Override + public void setValue(RecentsViewContainer view, float v) { + view.setContentAlpha(v); + } + + @Override + public Float get(RecentsViewContainer view) { + return view.mRecentsView.getContentAlpha(); + } + }; + + private final Rect mTempRect = new Rect(); + + private RecentsView mRecentsView; + private ClearAllButton mClearAllButton; + + public RecentsViewContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mClearAllButton = findViewById(R.id.clear_all_button); + mClearAllButton.setOnClickListener((v) -> { + mRecentsView.mActivity.getUserEventDispatcher() + .logActionOnControl(TAP, CLEAR_ALL_BUTTON); + mRecentsView.dismissAllTasks(); + }); + + mRecentsView = findViewById(R.id.overview_panel); + mClearAllButton.forceHasOverlappingRendering(false); + + mRecentsView.setClearAllButton(mClearAllButton); + mClearAllButton.setRecentsView(mRecentsView); + + if (mRecentsView.isRtl()) { + mClearAllButton.setNextFocusRightId(mRecentsView.getId()); + mRecentsView.setNextFocusLeftId(mClearAllButton.getId()); + } else { + mClearAllButton.setNextFocusLeftId(mRecentsView.getId()); + mRecentsView.setNextFocusRightId(mClearAllButton.getId()); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + mRecentsView.getTaskSize(mTempRect); + + mClearAllButton.setTranslationX( + (mRecentsView.isRtl() ? 1 : -1) * + (getResources().getDimension(R.dimen.clear_all_container_width) + - mClearAllButton.getMeasuredWidth()) / 2); + mClearAllButton.setTranslationY( + mTempRect.top + (mTempRect.height() - mClearAllButton.getMeasuredHeight()) / 2 + - mClearAllButton.getTop()); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + super.onTouchEvent(ev); + // Do not let touch escape to siblings below this view. This prevents scrolling of the + // workspace while in Recents. + return true; + } + + public void setContentAlpha(float alpha) { + if (alpha == mRecentsView.getContentAlpha()) { + return; + } + mRecentsView.setContentAlpha(alpha); + setVisibility(alpha > 0 ? VISIBLE : GONE); + } + + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + if (mRecentsView.getChildCount() > 0) { + // Carousel is first in tab order. + views.add(mRecentsView); + views.add(mClearAllButton); + } + } + + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + return mRecentsView.requestFocus(direction, previouslyFocusedRect) || + super.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public void addChildrenForAccessibility(ArrayList outChildren) { + outChildren.add(mRecentsView); + } +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java new file mode 100644 index 0000000000..c780b62340 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views; + +import static android.support.v4.graphics.ColorUtils.compositeColors; +import static android.support.v4.graphics.ColorUtils.setAlphaComponent; + +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Path.Op; +import android.util.AttributeSet; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.uioverrides.OverviewState; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ScrimView; + +/** + * Scrim used for all-apps and shelf in Overview + * In transposed layout, it behaves as a simple color scrim. + * In portrait layout, it draws a rounded rect such that + * From normal state to overview state, the shelf just fades in and does not move + * From overview state to all-apps state the shelf moves up and fades in to cover the screen + */ +public class ShelfScrimView extends ScrimView { + + // In transposed layout, we simply draw a flat color. + private boolean mDrawingFlatColor; + + // For shelf mode + private final int mEndAlpha; + private final int mThresholdAlpha; + private final float mRadius; + private final float mMaxScrimAlpha; + private final Paint mPaint; + + // Max vertical progress after which the scrim stops moving. + private float mMoveThreshold; + // Minimum visible size of the scrim. + private int mMinSize; + + private float mScrimMoveFactor = 0; + private int mShelfColor; + private int mRemainingScreenColor; + + private final Path mTempPath = new Path(); + private final Path mRemainingScreenPath = new Path(); + private boolean mRemainingScreenPathValid = false; + + public ShelfScrimView(Context context, AttributeSet attrs) { + super(context, attrs); + mMaxScrimAlpha = OVERVIEW.getWorkspaceScrimAlpha(mLauncher); + + mEndAlpha = Color.alpha(mEndScrim); + mThresholdAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha); + mRadius = mLauncher.getResources().getDimension(R.dimen.shelf_surface_radius); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + // Just assume the easiest UI for now, until we have the proper layout information. + mDrawingFlatColor = true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mRemainingScreenPathValid = false; + } + + @Override + public void reInitUi() { + DeviceProfile dp = mLauncher.getDeviceProfile(); + mDrawingFlatColor = dp.isVerticalBarLayout(); + + if (!mDrawingFlatColor) { + float swipeLength = OverviewState.getDefaultSwipeHeight(mLauncher); + mMoveThreshold = 1 - swipeLength / mLauncher.getAllAppsController().getShiftRange(); + mMinSize = dp.hotseatBarSizePx + dp.getInsets().bottom; + mRemainingScreenPathValid = false; + updateColors(); + } + updateDragHandleAlpha(); + invalidate(); + } + + @Override + public void updateColors() { + super.updateColors(); + if (mDrawingFlatColor) { + return; + } + + if (mProgress >= mMoveThreshold) { + mScrimMoveFactor = 1; + + if (mProgress >= 1) { + mShelfColor = 0; + } else { + int alpha = Math.round(mThresholdAlpha * ACCEL_2.getInterpolation( + (1 - mProgress) / (1 - mMoveThreshold))); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + } + + mRemainingScreenColor = 0; + } else if (mProgress <= 0) { + mScrimMoveFactor = 0; + mShelfColor = mCurrentFlatColor; + mRemainingScreenColor = 0; + + } else { + mScrimMoveFactor = mProgress / mMoveThreshold; + mRemainingScreenColor = setAlphaComponent(mScrimColor, + Math.round((1 - mScrimMoveFactor) * mMaxScrimAlpha * 255)); + + // Merge the remainingScreenColor and shelfColor in one to avoid overdraw. + int alpha = mEndAlpha - Math.round((mEndAlpha - mThresholdAlpha) * mScrimMoveFactor); + mShelfColor = compositeColors(setAlphaComponent(mEndScrim, alpha), + mRemainingScreenColor); + } + } + + @Override + protected void onDraw(Canvas canvas) { + float translate = drawBackground(canvas); + + if (mDragHandle != null) { + canvas.translate(0, -translate); + mDragHandle.draw(canvas); + canvas.translate(0, translate); + } + } + + private float drawBackground(Canvas canvas) { + if (mDrawingFlatColor) { + if (mCurrentFlatColor != 0) { + canvas.drawColor(mCurrentFlatColor); + } + return 0; + } + + if (mShelfColor == 0) { + return 0; + } else if (mScrimMoveFactor <= 0) { + canvas.drawColor(mShelfColor); + return getHeight(); + } + + float minTop = getHeight() - mMinSize; + float top = minTop * mScrimMoveFactor - mDragHandleSize; + + // Draw the scrim over the remaining screen if needed. + if (mRemainingScreenColor != 0) { + if (!mRemainingScreenPathValid) { + mTempPath.reset(); + // Using a arbitrary '+10' in the bottom to avoid any left-overs at the + // corners due to rounding issues. + mTempPath.addRoundRect(0, minTop, getWidth(), getHeight() + mRadius + 10, + mRadius, mRadius, Direction.CW); + + mRemainingScreenPath.reset(); + mRemainingScreenPath.addRect(0, 0, getWidth(), getHeight(), Direction.CW); + mRemainingScreenPath.op(mTempPath, Op.DIFFERENCE); + } + + float offset = minTop - top; + canvas.translate(0, -offset); + mPaint.setColor(mRemainingScreenColor); + canvas.drawPath(mRemainingScreenPath, mPaint); + canvas.translate(0, offset); + } + + mPaint.setColor(mShelfColor); + canvas.drawRoundRect(0, top, getWidth(), getHeight() + mRadius, + mRadius, mRadius, mPaint); + return minTop - mDragHandleSize - top; + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java new file mode 100644 index 0000000000..dd90c8867d --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Outline; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.TextView; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.shortcuts.DeepShortcutView; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; + +/** + * Contains options for a recent task when long-pressing its icon. + */ +public class TaskMenuView extends AbstractFloatingView { + + private static final Rect sTempRect = new Rect(); + + /** Note that these will be shown in order from top to bottom, if available for the task. */ + public static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[] { + new TaskSystemShortcut.AppInfo(), + new TaskSystemShortcut.SplitScreen(), + new TaskSystemShortcut.Pin(), + new TaskSystemShortcut.Install(), + }; + + private static final long OPEN_CLOSE_DURATION = 220; + + private BaseDraggingActivity mActivity; + private TextView mTaskIconAndName; + private AnimatorSet mOpenCloseAnimator; + private TaskView mTaskView; + + public TaskMenuView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mActivity = BaseDraggingActivity.fromContext(context); + setClipToOutline(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + float r = getResources().getDimensionPixelSize(R.dimen.task_menu_background_radius); + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), r); + } + }); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTaskIconAndName = findViewById(R.id.task_icon_and_name); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + BaseDragLayer dl = mActivity.getDragLayer(); + if (!dl.isEventOverView(this, ev)) { + // TODO: log this once we have a new container type for it? + close(true); + return true; + } + } + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (animate) { + animateClose(); + } else { + closeComplete(); + } + } + + @Override + public void logActionCommand(int command) { + // TODO + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_TASK_MENU) != 0; + } + + public static boolean showForTask(TaskView taskView) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext()); + final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( + R.layout.task_menu, activity.getDragLayer(), false); + return taskMenuView.populateAndShowForTask(taskView); + } + + private boolean populateAndShowForTask(TaskView taskView) { + if (isAttachedToWindow()) { + return false; + } + mActivity.getDragLayer().addView(this); + mTaskView = taskView; + addMenuOptions(mTaskView); + orientAroundTaskView(mTaskView); + post(this::animateOpen); + return true; + } + + private void addMenuOptions(TaskView taskView) { + Drawable icon = taskView.getTask().icon.getConstantState().newDrawable(); + int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size); + icon.setBounds(0, 0, iconSize, iconSize); + mTaskIconAndName.setCompoundDrawables(null, icon, null, null); + mTaskIconAndName.setText(TaskUtils.getTitle(getContext(), taskView.getTask())); + mTaskIconAndName.setOnClickListener(v -> close(true)); + + for (TaskSystemShortcut menuOption : MENU_OPTIONS) { + OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, taskView); + if (onClickListener != null) { + addMenuOption(menuOption, onClickListener); + } + } + } + + private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) { + DeepShortcutView menuOptionView = (DeepShortcutView) mActivity.getLayoutInflater().inflate( + R.layout.system_shortcut, this, false); + menuOptionView.getIconView().setBackgroundResource(menuOption.iconResId); + menuOptionView.getBubbleText().setText(menuOption.labelResId); + menuOptionView.setOnClickListener(onClickListener); + addView(menuOptionView); + } + + private void orientAroundTaskView(TaskView taskView) { + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect); + Rect insets = mActivity.getDragLayer().getInsets(); + int x = sTempRect.left + (sTempRect.width() - getMeasuredWidth()) / 2 - insets.left; + setX(Utilities.isRtl(getResources()) ? -x : x); + setY(sTempRect.top - mTaskIconAndName.getPaddingTop() - insets.top); + } + + private void animateOpen() { + animateOpenOrClosed(false); + mIsOpen = true; + } + + private void animateClose() { + animateOpenOrClosed(true); + } + + private void animateOpenOrClosed(boolean closing) { + if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { + return; + } + mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet(); + mOpenCloseAnimator.play(createOpenCloseOutlineProvider() + .createRevealAnimator(this, closing)); + mOpenCloseAnimator.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationStart(Animator animation) { + setVisibility(VISIBLE); + } + + @Override + public void onAnimationSuccess(Animator animator) { + if (closing) { + closeComplete(); + } + } + }); + mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); + mOpenCloseAnimator.setDuration(OPEN_CLOSE_DURATION); + mOpenCloseAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); + mOpenCloseAnimator.start(); + } + + private void closeComplete() { + mIsOpen = false; + mActivity.getDragLayer().removeView(this); + } + + private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { + int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size); + float fromRadius = iconSize / 2; + float toRadius = getResources().getDimensionPixelSize( + R.dimen.task_menu_background_radius); + Point iconCenter = new Point(getWidth() / 2, mTaskIconAndName.getPaddingTop() + iconSize / 2); + Rect fromRect = new Rect(iconCenter.x, iconCenter.y, iconCenter.x, iconCenter.y); + Rect toRect = new Rect(0, 0, getWidth(), getHeight()); + return new RoundedRectRevealOutlineProvider(fromRadius, toRadius, fromRect, toRect) { + @Override + public boolean shouldRemoveElevationDuringAnimation() { + return true; + } + }; + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java new file mode 100644 index 0000000000..d9dfd1815d --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; +import android.support.v4.graphics.ColorUtils; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Property; +import android.view.View; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.quickstep.TaskOverlayFactory; +import com.android.quickstep.TaskOverlayFactory.TaskOverlay; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +/** + * A task in the Recents view. + */ +public class TaskThumbnailView extends View { + + private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256]; + private static final LightingColorFilter[] sHighlightFilterCache = new LightingColorFilter[256]; + + public static final Property DIM_ALPHA_MULTIPLIER = + new FloatProperty("dimAlphaMultiplier") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlphaMultiplier) { + thumbnail.setDimAlphaMultipler(dimAlphaMultiplier); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlphaMultiplier; + } + }; + + private final float mCornerRadius; + + private final BaseActivity mActivity; + private final TaskOverlay mOverlay; + private final boolean mIsDarkTextTheme; + private final Paint mPaint = new Paint(); + private final Paint mBackgroundPaint = new Paint(); + + private final Matrix mMatrix = new Matrix(); + + private float mClipBottom = -1; + + private Task mTask; + private ThumbnailData mThumbnailData; + protected BitmapShader mBitmapShader; + + private float mDimAlpha = 1f; + private float mDimAlphaMultiplier = 1f; + + public TaskThumbnailView(Context context) { + this(context, null); + } + + public TaskThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius); + mOverlay = TaskOverlayFactory.get(context).createOverlay(this); + mPaint.setFilterBitmap(true); + mBackgroundPaint.setColor(Color.WHITE); + mActivity = BaseActivity.fromContext(context); + mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); + } + + public void bind() { + mOverlay.reset(); + } + + /** + * Updates this thumbnail. + */ + public void setThumbnail(Task task, ThumbnailData thumbnailData) { + mTask = task; + int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; + mPaint.setColor(color); + mBackgroundPaint.setColor(color); + + if (thumbnailData != null && thumbnailData.thumbnail != null) { + Bitmap bm = thumbnailData.thumbnail; + bm.prepareToDraw(); + mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mPaint.setShader(mBitmapShader); + mThumbnailData = thumbnailData; + updateThumbnailMatrix(); + } else { + mBitmapShader = null; + mThumbnailData = null; + mPaint.setShader(null); + mOverlay.reset(); + } + updateThumbnailPaintFilter(); + } + + public void setDimAlphaMultipler(float dimAlphaMultipler) { + mDimAlphaMultiplier = dimAlphaMultipler; + setDimAlpha(mDimAlpha); + } + + /** + * Sets the alpha of the dim layer on top of this view. + * + * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. + */ + public void setDimAlpha(float dimAlpha) { + mDimAlpha = dimAlpha; + updateThumbnailPaintFilter(); + } + + public Rect getInsets() { + if (mThumbnailData != null) { + return mThumbnailData.insets; + } + return new Rect(); + } + + public int getSysUiStatusNavFlags() { + if (mThumbnailData != null) { + int flags = 0; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_STATUS + : SystemUiController.FLAG_DARK_STATUS; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_NAV + : SystemUiController.FLAG_DARK_NAV; + return flags; + } + return 0; + } + + @Override + protected void onDraw(Canvas canvas) { + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + } + + public float getCornerRadius() { + return mCornerRadius; + } + + public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, + float cornerRadius) { + // Draw the background in all cases, except when the thumbnail data is opaque + final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null + || mThumbnailData == null; + if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); + if (drawBackgroundOnly) { + return; + } + } + + if (mClipBottom > 0) { + canvas.save(); + canvas.clipRect(x, y, width, mClipBottom); + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + canvas.restore(); + } else { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + } + } + + private void updateThumbnailPaintFilter() { + int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); + if (mBitmapShader != null) { + LightingColorFilter filter = getDimmingColorFilter(mul, mIsDarkTextTheme); + mPaint.setColorFilter(filter); + mBackgroundPaint.setColorFilter(filter); + } else { + mPaint.setColorFilter(null); + mPaint.setColor(Color.argb(255, mul, mul, mul)); + } + invalidate(); + } + + private void updateThumbnailMatrix() { + boolean rotate = false; + mClipBottom = -1; + if (mBitmapShader != null && mThumbnailData != null) { + float scale = mThumbnailData.scale; + Rect thumbnailInsets = mThumbnailData.insets; + final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - + (thumbnailInsets.left + thumbnailInsets.right) * scale; + final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - + (thumbnailInsets.top + thumbnailInsets.bottom) * scale; + + final float thumbnailScale; + final DeviceProfile profile = mActivity.getDeviceProfile(); + + if (getMeasuredWidth() == 0) { + // If we haven't measured , skip the thumbnail drawing and only draw the background + // color + thumbnailScale = 0f; + } else { + final Configuration configuration = + getContext().getResources().getConfiguration(); + // Rotate the screenshot if not in multi-window mode + rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && + configuration.orientation != mThumbnailData.orientation && + !mActivity.isInMultiWindowModeCompat() && + mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; + // Scale the screenshot to always fit the width of the card. + thumbnailScale = rotate + ? getMeasuredWidth() / thumbnailHeight + : getMeasuredWidth() / thumbnailWidth; + } + + if (rotate) { + int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; + mMatrix.setRotate(90 * rotationDir); + int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top; + int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right; + mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale); + if (rotationDir == -1) { + // Crop the right/bottom side of the screenshot rather than left/top + float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight(); + mMatrix.postTranslate(0, -excessHeight); + } + // Move the screenshot to the thumbnail window (rotation moved it out). + if (rotationDir == 1) { + mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0); + } else { + mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth()); + } + } else { + mMatrix.setTranslate(-mThumbnailData.insets.left * scale, + -mThumbnailData.insets.top * scale); + } + mMatrix.postScale(thumbnailScale, thumbnailScale); + mBitmapShader.setLocalMatrix(mMatrix); + + float bitmapHeight = Math.max((rotate ? thumbnailWidth : thumbnailHeight) + * thumbnailScale, 0); + if (Math.round(bitmapHeight) < getMeasuredHeight()) { + mClipBottom = bitmapHeight; + } + mPaint.setShader(mBitmapShader); + } + + if (rotate) { + // The overlay doesn't really work when the screenshot is rotated, so don't add it. + mOverlay.reset(); + } else { + mOverlay.setTaskInfo(mTask, mThumbnailData, mMatrix); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateThumbnailMatrix(); + } + + private static LightingColorFilter getDimmingColorFilter(int intensity, boolean shouldLighten) { + intensity = Utilities.boundToRange(intensity, 0, 255); + if (intensity == 255) { + return null; + } + if (shouldLighten) { + if (sHighlightFilterCache[intensity] == null) { + int colorAdd = 255 - intensity; + sHighlightFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), + Color.argb(255, colorAdd, colorAdd, colorAdd)); + } + return sHighlightFilterCache[intensity]; + } else { + if (sDimFilterCache[intensity] == null) { + sDimFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), 0); + } + return sDimFilterCache[intensity]; + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java new file mode 100644 index 0000000000..b5f31b8e2e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static android.widget.Toast.LENGTH_SHORT; + +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.os.Bundle; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.Toast; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.R; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.views.RecentsView.PageCallbacks; +import com.android.quickstep.views.RecentsView.ScrollState; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskCallbacks; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +import java.util.function.Consumer; + +/** + * A task in the Recents view. + */ +public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks { + + private static final String TAG = TaskView.class.getSimpleName(); + + /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ + private static final TimeInterpolator CURVE_INTERPOLATOR + = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; + + /** + * The alpha of a black scrim on a page in the carousel as it leaves the screen. + * In the resting position of the carousel, the adjacent pages have about half this scrim. + */ + private static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; + + /** + * How much to scale down pages near the edge of the screen. + */ + private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; + + public static final long SCALE_ICON_DURATION = 120; + private static final long DIM_ANIM_DURATION = 700; + + public static final Property ZOOM_SCALE = + new FloatProperty("zoomScale") { + @Override + public void setValue(TaskView taskView, float v) { + taskView.setZoomScale(v); + } + + @Override + public Float get(TaskView taskView) { + return taskView.mZoomScale; + } + }; + + private Task mTask; + private TaskThumbnailView mSnapshotView; + private IconView mIconView; + private float mCurveScale; + private float mZoomScale; + private Animator mDimAlphaAnim; + + public TaskView(Context context) { + this(context, null); + } + + public TaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOnClickListener((view) -> { + if (getTask() == null) { + return; + } + launchTask(true /* animate */); + BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss( + Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this), + TaskUtils.getComponentKeyForTask(getTask().key)); + }); + setOutlineProvider(new TaskOutlineProvider(getResources())); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSnapshotView = findViewById(R.id.snapshot); + mIconView = findViewById(R.id.icon); + } + + /** + * Updates this task view to the given {@param task}. + */ + public void bind(Task task) { + if (mTask != null) { + mTask.removeCallback(this); + } + mTask = task; + mSnapshotView.bind(); + task.addCallback(this); + setContentDescription(task.titleDescription); + } + + public Task getTask() { + return mTask; + } + + public TaskThumbnailView getThumbnail() { + return mSnapshotView; + } + + public IconView getIconView() { + return mIconView; + } + + public void launchTask(boolean animate) { + launchTask(animate, (result) -> { + if (!result) { + notifyTaskLaunchFailed(TAG); + } + }, getHandler()); + } + + public void launchTask(boolean animate, Consumer resultCallback, + Handler resultCallbackHandler) { + if (mTask != null) { + final ActivityOptions opts; + if (animate) { + opts = BaseDraggingActivity.fromContext(getContext()) + .getActivityLaunchOptions(this); + } else { + opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0); + } + ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, + opts, resultCallback, resultCallbackHandler); + } + } + + @Override + public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { + mSnapshotView.setThumbnail(task, thumbnailData); + mIconView.setDrawable(task.icon); + mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this)); + mIconView.setOnLongClickListener(icon -> { + requestDisallowInterceptTouchEvent(true); + return TaskMenuView.showForTask(this); + }); + } + + @Override + public void onTaskDataUnloaded() { + mSnapshotView.setThumbnail(null, null); + mIconView.setDrawable(null); + mIconView.setOnLongClickListener(null); + } + + @Override + public void onTaskWindowingModeChanged() { + // Do nothing + } + + public void animateIconToScaleAndDim(float scale) { + mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start(); + mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale, + scale); + mDimAlphaAnim.setDuration(DIM_ANIM_DURATION); + mDimAlphaAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDimAlphaAnim = null; + } + }); + mDimAlphaAnim.start(); + } + + protected void setIconScaleAndDim(float iconScale) { + mIconView.animate().cancel(); + mIconView.setScaleX(iconScale); + mIconView.setScaleY(iconScale); + if (mDimAlphaAnim != null) { + mDimAlphaAnim.cancel(); + } + mSnapshotView.setDimAlphaMultipler(iconScale); + } + + public void resetVisualProperties() { + setZoomScale(1); + setTranslationX(0f); + setTranslationY(0f); + setTranslationZ(0); + setAlpha(1f); + setIconScaleAndDim(1); + } + + @Override + public void onPageScroll(ScrollState scrollState) { + float curveInterpolation = + CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); + + mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); + setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setPivotX((right - left) * 0.5f); + setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); + } + + public static float getCurveScaleForInterpolation(float linearInterpolation) { + float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); + return getCurveScaleForCurveInterpolation(curveInterpolation); + } + + private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { + return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; + } + + private void setCurveScale(float curveScale) { + mCurveScale = curveScale; + onScaleChanged(); + } + + public float getCurveScale() { + return mCurveScale; + } + + public void setZoomScale(float adjacentScale) { + mZoomScale = adjacentScale; + onScaleChanged(); + } + + private void onScaleChanged() { + float scale = mCurveScale * mZoomScale; + setScaleX(scale); + setScaleY(scale); + } + + @Override + public boolean hasOverlappingRendering() { + // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. + return false; + } + + private static final class TaskOutlineProvider extends ViewOutlineProvider { + + private final int mMarginTop; + private final float mRadius; + + TaskOutlineProvider(Resources res) { + mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + mRadius = res.getDimension(R.dimen.task_corner_radius); + } + + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, mMarginTop, view.getWidth(), + view.getHeight(), mRadius); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + info.addAction( + new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task, + getContext().getText(R.string.accessibility_close_task))); + + final Context context = getContext(); + final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(context); + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + OnClickListener onClickListener = menuOption.getOnClickListener(activity, this); + if (onClickListener != null) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId, + context.getText(menuOption.labelResId))); + } + } + + final RecentsView recentsView = getRecentsView(); + final AccessibilityNodeInfo.CollectionItemInfo itemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain( + 0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1, + false); + info.setCollectionItemInfo(itemInfo); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action == R.string.accessibility_close_task) { + getRecentsView().dismissTask(this, true /*animateTaskView*/, + true /*removeTask*/); + return true; + } + + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + if (action == menuOption.labelResId) { + OnClickListener onClickListener = menuOption.getOnClickListener( + BaseDraggingActivity.fromContext(getContext()), this); + if (onClickListener != null) { + onClickListener.onClick(this); + } + return true; + } + } + + if (getRecentsView().performTaskAccessibilityActionExtra(action)) return true; + + return super.performAccessibilityAction(action, arguments); + } + + private RecentsView getRecentsView() { + return (RecentsView) getParent(); + } + + public void notifyTaskLaunchFailed(String tag) { + String msg = "Failed to launch task"; + if (mTask != null) { + msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; + } + Log.w(tag, msg); + Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); + } +} diff --git a/res/animator-v23/discovery_bounce.xml b/res/animator-v23/discovery_bounce.xml index 8d0e8fdfb5..f5548537b1 100644 --- a/res/animator-v23/discovery_bounce.xml +++ b/res/animator-v23/discovery_bounce.xml @@ -26,14 +26,14 @@ android:fraction="0" android:value="1f" /> + android:value="0.9738f" /> + + + + + \ No newline at end of file diff --git a/res/drawable-hdpi/work_tab_user_education.png b/res/drawable-hdpi/work_tab_user_education.png new file mode 100644 index 0000000000..1879dfb2b7 Binary files /dev/null and b/res/drawable-hdpi/work_tab_user_education.png differ diff --git a/res/drawable-mdpi/work_tab_user_education.png b/res/drawable-mdpi/work_tab_user_education.png new file mode 100644 index 0000000000..65c7e638dd Binary files /dev/null and b/res/drawable-mdpi/work_tab_user_education.png differ diff --git a/res/drawable-v24/ic_info_shadow.xml b/res/drawable-v24/ic_setup_shadow.xml similarity index 94% rename from res/drawable-v24/ic_info_shadow.xml rename to res/drawable-v24/ic_setup_shadow.xml index 1fe2c46b4b..10aeee6e02 100644 --- a/res/drawable-v24/ic_info_shadow.xml +++ b/res/drawable-v24/ic_setup_shadow.xml @@ -15,5 +15,5 @@ --> diff --git a/res/drawable-xhdpi/work_tab_user_education.png b/res/drawable-xhdpi/work_tab_user_education.png new file mode 100644 index 0000000000..59df7a8647 Binary files /dev/null and b/res/drawable-xhdpi/work_tab_user_education.png differ diff --git a/res/drawable-xxhdpi/work_tab_user_education.png b/res/drawable-xxhdpi/work_tab_user_education.png new file mode 100644 index 0000000000..3c6aa208ea Binary files /dev/null and b/res/drawable-xxhdpi/work_tab_user_education.png differ diff --git a/res/drawable/all_apps_search_divider.xml b/res/drawable/bg_all_apps_searchbox.xml similarity index 78% rename from res/drawable/all_apps_search_divider.xml rename to res/drawable/bg_all_apps_searchbox.xml index 99905e4233..c3249279af 100644 --- a/res/drawable/all_apps_search_divider.xml +++ b/res/drawable/bg_all_apps_searchbox.xml @@ -1,5 +1,5 @@ - - - - + + + \ No newline at end of file diff --git a/res/drawable/bg_deferred_app_widget.xml b/res/drawable/bg_deferred_app_widget.xml new file mode 100644 index 0000000000..07bae480b2 --- /dev/null +++ b/res/drawable/bg_deferred_app_widget.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/res/drawable/bg_notification_content.xml b/res/drawable/bg_notification_content.xml new file mode 100644 index 0000000000..cf129eba82 --- /dev/null +++ b/res/drawable/bg_notification_content.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/res/drawable/drag_handle_indicator.xml b/res/drawable/drag_handle_indicator.xml new file mode 100644 index 0000000000..b01b84ab81 --- /dev/null +++ b/res/drawable/drag_handle_indicator.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml new file mode 100644 index 0000000000..8b2f55fb32 --- /dev/null +++ b/res/drawable/ic_close.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_star_rating.xml b/res/drawable/ic_corp.xml similarity index 60% rename from res/drawable/ic_star_rating.xml rename to res/drawable/ic_corp.xml index 4e34fa33e8..48f50076c9 100644 --- a/res/drawable/ic_star_rating.xml +++ b/res/drawable/ic_corp.xml @@ -14,14 +14,11 @@ limitations under the License. --> - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z" + android:fillColor="?android:attr/textColorHint"/> \ No newline at end of file diff --git a/res/drawable/ic_install_no_shadow.xml b/res/drawable/ic_install_no_shadow.xml new file mode 100644 index 0000000000..ffce22aac1 --- /dev/null +++ b/res/drawable/ic_install_no_shadow.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/res/drawable/ic_setting.xml b/res/drawable/ic_setting.xml index 1bab18990e..a83aab3204 100644 --- a/res/drawable/ic_setting.xml +++ b/res/drawable/ic_setting.xml @@ -14,17 +14,30 @@ Copyright (C) 2016 The Android Open Source Project limitations under the License. --> + android:width="@dimen/options_menu_icon_size" + android:height="@dimen/options_menu_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/textColorPrimary" > + android:fillColor="#FFFFFFFF" + android:pathData="M13.85,22.25h-3.7c-0.74,0-1.36-0.54-1.45-1.27l-0.27-1.89c-0.27-0.14-0.53-0.29-0.79-0.46l-1.8,0.72 + c-0.7,0.26-1.47-0.03-1.81-0.65L2.2,15.53c-0.35-0.66-0.2-1.44,0.36-1.88l1.53-1.19c-0.01-0.15-0.02-0.3-0.02-0.46 + c0-0.15,0.01-0.31,0.02-0.46l-1.52-1.19C1.98,9.9,1.83,9.09,2.2,8.47l1.85-3.19c0.34-0.62,1.11-0.9,1.79-0.63l1.81,0.73 + c0.26-0.17,0.52-0.32,0.78-0.46l0.27-1.91c0.09-0.7,0.71-1.25,1.44-1.25h3.7c0.74,0,1.36,0.54,1.45,1.27l0.27,1.89 + c0.27,0.14,0.53,0.29,0.79,0.46l1.8-0.72c0.71-0.26,1.48,0.03,1.82,0.65l1.84,3.18c0.36,0.66,0.2,1.44-0.36,1.88l-1.52,1.19 + c0.01,0.15,0.02,0.3,0.02,0.46s-0.01,0.31-0.02,0.46l1.52,1.19c0.56,0.45,0.72,1.23,0.37,1.86l-1.86,3.22 + c-0.34,0.62-1.11,0.9-1.8,0.63l-1.8-0.72c-0.26,0.17-0.52,0.32-0.78,0.46l-0.27,1.91C15.21,21.71,14.59,22.25,13.85,22.25z + M13.32,20.72c0,0.01,0,0.01,0,0.02L13.32,20.72z M10.68,20.7l0,0.02C10.69,20.72,10.69,20.71,10.68,20.7z M10.62,20.25h2.76 + l0.37-2.55l0.53-0.22c0.44-0.18,0.88-0.44,1.34-0.78l0.45-0.34l2.38,0.96l1.38-2.4l-2.03-1.58l0.07-0.56 + c0.03-0.26,0.06-0.51,0.06-0.78c0-0.27-0.03-0.53-0.06-0.78l-0.07-0.56l2.03-1.58l-1.39-2.4l-2.39,0.96l-0.45-0.35 + c-0.42-0.32-0.87-0.58-1.33-0.77L13.75,6.3l-0.37-2.55h-2.76L10.25,6.3L9.72,6.51C9.28,6.7,8.84,6.95,8.38,7.3L7.93,7.63 + L5.55,6.68L4.16,9.07l2.03,1.58l-0.07,0.56C6.09,11.47,6.06,11.74,6.06,12c0,0.26,0.02,0.53,0.06,0.78l0.07,0.56l-2.03,1.58 + l1.38,2.4l2.39-0.96l0.45,0.35c0.43,0.33,0.86,0.58,1.33,0.77l0.53,0.22L10.62,20.25z M18.22,17.72c0,0.01-0.01,0.02-0.01,0.03 + L18.22,17.72z M5.77,17.71l0.01,0.02C5.78,17.72,5.77,17.71,5.77,17.71z M3.93,9.47L3.93,9.47C3.93,9.47,3.93,9.47,3.93,9.47z + M18.22,6.27c0,0.01,0.01,0.02,0.01,0.02L18.22,6.27z M5.79,6.25L5.78,6.27C5.78,6.27,5.79,6.26,5.79,6.25z M13.31,3.28 + c0,0.01,0,0.01,0,0.02L13.31,3.28z M10.69,3.26l0,0.02C10.69,3.27,10.69,3.27,10.69,3.26z"/> + diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml index 2a86e1042d..37632d1a4b 100644 --- a/res/drawable/ic_uninstall_no_shadow.xml +++ b/res/drawable/ic_uninstall_no_shadow.xml @@ -21,6 +21,11 @@ android:tint="?android:attr/textColorPrimary" > + android:pathData="M15,4V3H9v1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4H15z M17,19H7V6h10V19z" /> + + diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml index 9e9222f645..7fd93403c5 100644 --- a/res/drawable/ic_wallpaper.xml +++ b/res/drawable/ic_wallpaper.xml @@ -14,16 +14,13 @@ Copyright (C) 2016 The Android Open Source Project limitations under the License. --> + android:width="@dimen/options_menu_icon_size" + android:height="@dimen/options_menu_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z + M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1 + c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/> diff --git a/res/drawable/ic_widget.xml b/res/drawable/ic_widget.xml index de2980f09c..3ebbb68bb9 100644 --- a/res/drawable/ic_widget.xml +++ b/res/drawable/ic_widget.xml @@ -14,13 +14,12 @@ Copyright (C) 2016 The Android Open Source Project limitations under the License. --> + android:width="@dimen/options_menu_icon_size" + android:height="@dimen/options_menu_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="?android:attr/textColorPrimary" + android:pathData="M16.66,4.52l2.83,2.83l-2.83,2.83l-2.83-2.83L16.66,4.52 M9,5v4H5V5H9 M19,15v4h-4v-4H19 M9,15v4H5v-4H9 M16.66,1.69 + L11,7.34L16.66,13l5.66-5.66L16.66,1.69L16.66,1.69z M11,3H3v8h8V7.34V3L11,3z M21,13h-4.34H13v8h8V13L21,13z M11,13H3v8h8V13L11,13z"/> diff --git a/res/drawable/round_rect_primary.xml b/res/drawable/round_rect_primary.xml index 2c47e06536..16310f8bdc 100644 --- a/res/drawable/round_rect_primary.xml +++ b/res/drawable/round_rect_primary.xml @@ -17,5 +17,5 @@ - + diff --git a/res/drawable/tooltip_frame.xml b/res/drawable/tooltip_frame.xml new file mode 100644 index 0000000000..03190518fa --- /dev/null +++ b/res/drawable/tooltip_frame.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/top_round_rect_primary.xml b/res/drawable/top_round_rect_primary.xml new file mode 100644 index 0000000000..1caaa026f0 --- /dev/null +++ b/res/drawable/top_round_rect_primary.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/res/layout-land/all_apps_fast_scroller.xml b/res/layout-land/all_apps_fast_scroller.xml deleted file mode 100644 index 6a68f84e98..0000000000 --- a/res/layout-land/all_apps_fast_scroller.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml deleted file mode 100644 index ac440fc01e..0000000000 --- a/res/layout-land/launcher.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout-sw720dp/all_apps_fast_scroller.xml b/res/layout-sw720dp/all_apps_fast_scroller.xml deleted file mode 100644 index 12c15cca9a..0000000000 --- a/res/layout-sw720dp/all_apps_fast_scroller.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml deleted file mode 100644 index 03e42bc1d0..0000000000 --- a/res/layout-sw720dp/launcher.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml index 39df2b193c..02d793e1c0 100644 --- a/res/layout/all_apps.xml +++ b/res/layout/all_apps.xml @@ -18,60 +18,21 @@ will bake the left/right padding into that view's background itself. --> + android:clipChildren="true" + android:clipToPadding="false" + android:focusable="false" + android:saveEnabled="false" > - + + - + - - - - - - - - - - + \ No newline at end of file diff --git a/res/layout/all_apps_discovery_item.xml b/res/layout/all_apps_discovery_item.xml deleted file mode 100644 index 728283fc11..0000000000 --- a/res/layout/all_apps_discovery_item.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/all_apps_discovery_loading_divider.xml b/res/layout/all_apps_discovery_loading_divider.xml deleted file mode 100644 index 005847c541..0000000000 --- a/res/layout/all_apps_discovery_loading_divider.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - \ 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 12c15cca9a..5537bc60a5 100644 --- a/res/layout/all_apps_fast_scroller.xml +++ b/res/layout/all_apps_fast_scroller.xml @@ -21,7 +21,7 @@ android:id="@+id/fast_scroller_popup" style="@style/FastScrollerPopup" android:layout_alignParentEnd="true" - android:layout_alignTop="@+id/apps_list_view" + android:layout_below="@+id/search_container_all_apps" android:layout_marginEnd="@dimen/fastscroll_popup_margin" /> diff --git a/res/layout/all_apps_floating_header.xml b/res/layout/all_apps_floating_header.xml new file mode 100644 index 0000000000..c4240f80db --- /dev/null +++ b/res/layout/all_apps_floating_header.xml @@ -0,0 +1,58 @@ + + + + + + +